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
122 lines
3.1 KiB
Go
122 lines
3.1 KiB
Go
package weather
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
)
|
|
|
|
const brightSkyBaseURL = "https://api.brightsky.dev/weather"
|
|
|
|
// BrightSky implements Provider using the Bright Sky API (DWD MOSMIX wrapper).
|
|
type BrightSky struct {
|
|
client *http.Client
|
|
baseURL string
|
|
}
|
|
|
|
// NewBrightSky creates a new Bright Sky provider.
|
|
func NewBrightSky(client *http.Client) *BrightSky {
|
|
if client == nil {
|
|
client = &http.Client{Timeout: 30 * time.Second}
|
|
}
|
|
return &BrightSky{client: client, baseURL: brightSkyBaseURL}
|
|
}
|
|
|
|
func (b *BrightSky) Name() string { return "brightsky" }
|
|
|
|
type brightSkyResponse struct {
|
|
Weather []struct {
|
|
Timestamp string `json:"timestamp"`
|
|
Temperature *float64 `json:"temperature"`
|
|
Humidity *float64 `json:"relative_humidity"`
|
|
DewPoint *float64 `json:"dew_point"`
|
|
CloudCover *float64 `json:"cloud_cover"`
|
|
WindSpeed *float64 `json:"wind_speed"`
|
|
WindDirection *float64 `json:"wind_direction"`
|
|
Precipitation *float64 `json:"precipitation"`
|
|
Sunshine *float64 `json:"sunshine"`
|
|
PressureMSL *float64 `json:"pressure_msl"`
|
|
Condition string `json:"condition"`
|
|
} `json:"weather"`
|
|
}
|
|
|
|
func (b *BrightSky) FetchForecast(ctx context.Context, lat, lon float64, timezone string) (*ForecastResponse, error) {
|
|
now := time.Now()
|
|
params := url.Values{
|
|
"lat": {fmt.Sprintf("%.4f", lat)},
|
|
"lon": {fmt.Sprintf("%.4f", lon)},
|
|
"date": {now.Format("2006-01-02")},
|
|
"last_date": {now.Add(72 * time.Hour).Format("2006-01-02")},
|
|
}
|
|
|
|
reqURL := b.baseURL + "?" + params.Encode()
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build request: %w", err)
|
|
}
|
|
|
|
resp, err := b.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch brightsky: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("brightsky returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var raw brightSkyResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
|
return nil, fmt.Errorf("decode brightsky: %w", err)
|
|
}
|
|
|
|
result := &ForecastResponse{Source: "brightsky"}
|
|
for _, w := range raw.Weather {
|
|
t, err := time.Parse(time.RFC3339, w.Timestamp)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
hf := HourlyForecast{
|
|
Timestamp: t,
|
|
Condition: w.Condition,
|
|
}
|
|
if w.Temperature != nil {
|
|
hf.TemperatureC = *w.Temperature
|
|
}
|
|
if w.Humidity != nil {
|
|
hf.HumidityPct = *w.Humidity
|
|
}
|
|
if w.DewPoint != nil {
|
|
hf.DewPointC = *w.DewPoint
|
|
}
|
|
if w.CloudCover != nil {
|
|
hf.CloudCoverPct = *w.CloudCover
|
|
}
|
|
if w.WindSpeed != nil {
|
|
hf.WindSpeedMs = *w.WindSpeed / 3.6 // km/h -> m/s
|
|
}
|
|
if w.WindDirection != nil {
|
|
hf.WindDirectionDeg = *w.WindDirection
|
|
}
|
|
if w.Precipitation != nil {
|
|
hf.PrecipitationMm = *w.Precipitation
|
|
}
|
|
if w.Sunshine != nil {
|
|
hf.SunshineMin = *w.Sunshine
|
|
}
|
|
if w.PressureMSL != nil {
|
|
hf.PressureHpa = *w.PressureMSL
|
|
}
|
|
// BrightSky doesn't provide IsDay — approximate from hour
|
|
hour := t.Hour()
|
|
hf.IsDay = hour >= 6 && hour < 21
|
|
|
|
result.Hourly = append(result.Hourly, hf)
|
|
}
|
|
|
|
return result, nil
|
|
}
|