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
93 lines
2.4 KiB
Go
93 lines
2.4 KiB
Go
package weather
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
const brightSkyTestJSON = `{
|
|
"weather": [
|
|
{
|
|
"timestamp": "2025-07-15T14:00:00+02:00",
|
|
"temperature": 34.5,
|
|
"relative_humidity": 40,
|
|
"dew_point": 19.5,
|
|
"cloud_cover": 15,
|
|
"wind_speed": 10.8,
|
|
"wind_direction": 220,
|
|
"precipitation": 0,
|
|
"sunshine": 55,
|
|
"pressure_msl": 1015.2,
|
|
"condition": "dry"
|
|
},
|
|
{
|
|
"timestamp": "2025-07-15T15:00:00+02:00",
|
|
"temperature": 35.1,
|
|
"relative_humidity": 38,
|
|
"dew_point": 19.0,
|
|
"cloud_cover": 10,
|
|
"wind_speed": 7.2,
|
|
"wind_direction": 210,
|
|
"precipitation": 0,
|
|
"sunshine": 60,
|
|
"pressure_msl": 1015.0,
|
|
"condition": "dry"
|
|
}
|
|
]
|
|
}`
|
|
|
|
func TestBrightSkyFetchForecast(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(brightSkyTestJSON))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
bs := NewBrightSky(srv.Client())
|
|
bs.baseURL = srv.URL
|
|
|
|
resp, err := bs.FetchForecast(context.Background(), 52.52, 13.41, "Europe/Berlin")
|
|
if err != nil {
|
|
t.Fatalf("FetchForecast: %v", err)
|
|
}
|
|
|
|
if resp.Source != "brightsky" {
|
|
t.Errorf("Source = %s, want brightsky", resp.Source)
|
|
}
|
|
if len(resp.Hourly) != 2 {
|
|
t.Fatalf("Hourly len = %d, want 2", len(resp.Hourly))
|
|
}
|
|
if resp.Hourly[0].TemperatureC != 34.5 {
|
|
t.Errorf("temp = %v, want 34.5", resp.Hourly[0].TemperatureC)
|
|
}
|
|
// wind_speed is km/h in brightsky, converted to m/s
|
|
expectedWindMs := 10.8 / 3.6
|
|
if diff := resp.Hourly[0].WindSpeedMs - expectedWindMs; diff > 0.01 || diff < -0.01 {
|
|
t.Errorf("WindSpeedMs = %v, want %v", resp.Hourly[0].WindSpeedMs, expectedWindMs)
|
|
}
|
|
if resp.Hourly[0].Condition != "dry" {
|
|
t.Errorf("Condition = %s, want dry", resp.Hourly[0].Condition)
|
|
}
|
|
// 14:00 should be daytime
|
|
if !resp.Hourly[0].IsDay {
|
|
t.Error("expected IsDay=true for hour 14")
|
|
}
|
|
}
|
|
|
|
func TestBrightSkyServerError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
bs := NewBrightSky(srv.Client())
|
|
bs.baseURL = srv.URL
|
|
|
|
_, err := bs.FetchForecast(context.Background(), 52.52, 13.41, "Europe/Berlin")
|
|
if err == nil {
|
|
t.Error("expected error for 502 response")
|
|
}
|
|
}
|