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
89 lines
2.5 KiB
Go
89 lines
2.5 KiB
Go
package weather
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
const openMeteoTestJSON = `{
|
|
"hourly": {
|
|
"time": ["2025-07-15T00:00", "2025-07-15T01:00", "2025-07-15T02:00"],
|
|
"temperature_2m": [22.5, 21.8, 21.0],
|
|
"apparent_temperature": [23.1, 22.4, 21.5],
|
|
"relative_humidity_2m": [65, 68, 72],
|
|
"dew_point_2m": [15.5, 15.8, 16.0],
|
|
"cloud_cover": [20, 30, 45],
|
|
"wind_speed_10m": [3.5, 2.8, 2.1],
|
|
"wind_direction_10m": [180, 190, 200],
|
|
"precipitation": [0, 0, 0],
|
|
"sunshine_duration": [3600, 3000, 0],
|
|
"shortwave_radiation": [0, 0, 0],
|
|
"surface_pressure": [1013, 1013, 1012],
|
|
"is_day": [0, 0, 0]
|
|
},
|
|
"daily": {
|
|
"time": ["2025-07-15"],
|
|
"temperature_2m_max": [35.5],
|
|
"temperature_2m_min": [20.1],
|
|
"apparent_temperature_max": [37.2],
|
|
"sunrise": ["2025-07-15T05:15"],
|
|
"sunset": ["2025-07-15T21:30"]
|
|
}
|
|
}`
|
|
|
|
func TestOpenMeteoFetchForecast(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(openMeteoTestJSON))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
om := NewOpenMeteo(srv.Client())
|
|
om.baseURL = srv.URL
|
|
|
|
resp, err := om.FetchForecast(context.Background(), 52.52, 13.41, "Europe/Berlin")
|
|
if err != nil {
|
|
t.Fatalf("FetchForecast: %v", err)
|
|
}
|
|
|
|
if resp.Source != "openmeteo" {
|
|
t.Errorf("Source = %s, want openmeteo", resp.Source)
|
|
}
|
|
if len(resp.Hourly) != 3 {
|
|
t.Fatalf("Hourly len = %d, want 3", len(resp.Hourly))
|
|
}
|
|
if resp.Hourly[0].TemperatureC != 22.5 {
|
|
t.Errorf("Hourly[0].TemperatureC = %v, want 22.5", resp.Hourly[0].TemperatureC)
|
|
}
|
|
if resp.Hourly[0].ApparentTempC != 23.1 {
|
|
t.Errorf("Hourly[0].ApparentTempC = %v, want 23.1", resp.Hourly[0].ApparentTempC)
|
|
}
|
|
if resp.Hourly[0].SunshineMin != 60.0 {
|
|
t.Errorf("Hourly[0].SunshineMin = %v, want 60 (3600s / 60)", resp.Hourly[0].SunshineMin)
|
|
}
|
|
|
|
if len(resp.Daily) != 1 {
|
|
t.Fatalf("Daily len = %d, want 1", len(resp.Daily))
|
|
}
|
|
if resp.Daily[0].TempMaxC != 35.5 {
|
|
t.Errorf("Daily[0].TempMaxC = %v, want 35.5", resp.Daily[0].TempMaxC)
|
|
}
|
|
}
|
|
|
|
func TestOpenMeteoServerError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
om := NewOpenMeteo(srv.Client())
|
|
om.baseURL = srv.URL
|
|
|
|
_, err := om.FetchForecast(context.Background(), 52.52, 13.41, "Europe/Berlin")
|
|
if err == nil {
|
|
t.Error("expected error for 500 response")
|
|
}
|
|
}
|