Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 49 additions & 47 deletions backends/smhi.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,29 @@ import (
type smhiConfig struct {
}

type smhiDataPoint struct {
Level int `json:"level"`
LevelType string `json:"levelType"`
Name string `json:"name"`
Unit string `json:"unit"`
Values []interface{} `json:"values"`
type smhiData struct {
AirTemperature float64 `json:"air_temperature"`
WindFromDirection float64 `json:"wind_from_direction"`
WindSpeed float64 `json:"wind_speed"`
WindSpeedOfGust float64 `json:"wind_speed_of_gust"`
RelativeHumidity float64 `json:"relative_humidity"`
VisibilityInAir float64 `json:"visibility_in_air"`
PrecipitationAmountMean float64 `json:"precipitation_amount_mean"`
SymbolCode float64 `json:"symbol_code"`
}

type smhiTimeSeries struct {
ValidTime string `json:"validTime"`
Parameters []*smhiDataPoint `json:"parameters"`
Time string `json:"time"`
IntervalParametersStartTime string `json:"intervalParametersStartTime"`
Data smhiData `json:"data"`
}

type smhiGeometry struct {
Coordinates [][]float32 `json:"coordinates"`
Coordinates []float32 `json:"coordinates"`
}

type smhiResponse struct {
ApprovedTime string `json:"approvedTime"`
CreatedTime string `json:"createdTime"`
ReferenceTime string `json:"referenceTime"`
Geometry smhiGeometry `json:"geometry"`
TimeSeries []*smhiTimeSeries `json:"timeSeries"`
Expand All @@ -45,8 +49,8 @@ type smhiCondition struct {
}

const (
// see http://opendata.smhi.se/apidocs/metfcst/index.html
smhiWuri = "https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/%s/lat/%s/data.json"
// API Spec: https://opendata.smhi.se/metfcst/snow1gv1/parameters
smhiWuri = "https://opendata-download-metfcst.smhi.se/api/category/snow1g/version/1/geotype/point/lon/%s/lat/%s/data.json"
)
Comment on lines 51 to 54
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old smhiWuri assignment is left in place as a commented-out line. This is effectively dead code and can be confusing to maintainers; consider removing it (or turning it into a proper comment explaining the deprecation) to keep the constant definition unambiguous.

Copilot uses AI. Check for mistakes.

var (
Expand Down Expand Up @@ -109,7 +113,6 @@ func (c *smhiConfig) fetch(url string) (*smhiResponse, error) {
return nil, fmt.Errorf("Unable to parse response (%s): %v", url, err)
}
return &response, nil

}

func (c *smhiConfig) Fetch(location string, numDays int) (ret iface.Data) {
Expand All @@ -128,10 +131,11 @@ func (c *smhiConfig) Fetch(location string, numDays int) (ret iface.Data) {
ret.Current = c.parseCurrent(resp)
ret.Forecast = c.parseForecast(resp, numDays)
coordinates := resp.Geometry.Coordinates
ret.GeoLoc = &iface.LatLon{Latitude: coordinates[0][1], Longitude: coordinates[0][0]}
ret.GeoLoc = &iface.LatLon{Latitude: coordinates[1], Longitude: coordinates[0]}
ret.Location = location + " (Forecast provided by SMHI)"
return ret
}

func (c *smhiConfig) parseForecast(response *smhiResponse, numDays int) (days []iface.Day) {
if numDays > 10 {
numDays = 10
Expand All @@ -147,7 +151,7 @@ func (c *smhiConfig) parseForecast(response *smhiResponse, numDays int) (days []
break
}

ts, err := time.Parse(time.RFC3339, prediction.ValidTime)
ts, err := time.Parse(time.RFC3339, prediction.Time)
if err != nil {
Comment on lines 151 to 155
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Day grouping in parseForecast depends on comparing ts.Day() to currentTime.Day(), but currentTime/day.Date are initialized from time.Now() earlier in the function rather than the first forecast timestamp. If the first prediction.Time is in a different day/timezone than the local clock, this can mis-group days. Consider initializing currentTime/day.Date from the first parsed prediction.Time (and in a consistent timezone) before applying the grouping logic.

Copilot uses AI. Check for mistakes.
log.Fatalf("Failed to parse timestamp: %v\n", err)
}
Expand All @@ -172,7 +176,7 @@ func (c *smhiConfig) parseCurrent(forecast *smhiResponse) (cnd iface.Cond) {
var currentTime time.Time = time.Now().UTC()

for _, prediction := range forecast.TimeSeries {
ts, err := time.Parse(time.RFC3339, prediction.ValidTime)
ts, err := time.Parse(time.RFC3339, prediction.Time)
if err != nil {
log.Fatalf("Failed to parse timestamp: %v\n", err)
}
Expand All @@ -185,44 +189,42 @@ func (c *smhiConfig) parseCurrent(forecast *smhiResponse) (cnd iface.Cond) {
}

func (c *smhiConfig) parsePrediction(prediction *smhiTimeSeries) (cnd iface.Cond) {
ts, err := time.Parse(time.RFC3339, prediction.ValidTime)
ts, err := time.Parse(time.RFC3339, prediction.Time)
if err != nil {
log.Fatalf("Failed to parse timestamp: %v\n", err)
}
cnd.Time = ts

for _, param := range prediction.Parameters {
switch param.Name {
case "pmean":
precip := float32(param.Values[0].(float64) / 1000) // Convert mm/h to m/h
cnd.PrecipM = &precip
case "vis":
vis := float32(param.Values[0].(float64) * 1000) // Convert km to m
cnd.VisibleDistM = &vis
case "t":
temp := float32(param.Values[0].(float64))
cnd.TempC = &temp
case "Wsymb2":
condition := weatherConditions[int(param.Values[0].(float64))]
cnd.Code = condition.WeatherCode
cnd.Desc = condition.Description
case "ws":
windSpeed := float32(param.Values[0].(float64) * 3.6) // convert m/s to km/h
cnd.WindspeedKmph = &windSpeed
case "gust":
gustSpeed := float32(param.Values[0].(float64) * 3.6) // convert m/s to km/h
cnd.WindGustKmph = &gustSpeed
case "wd":
val := int(param.Values[0].(float64))
cnd.WinddirDegree = &val
case "r":
val := int(param.Values[0].(float64))
cnd.Humidity = &val
default:
continue
}
precip := float32(prediction.Data.PrecipitationAmountMean / 1000) // Convert mm/h to m/h
cnd.PrecipM = &precip

vis := float32(prediction.Data.VisibilityInAir * 1000) // Convert km to m
cnd.VisibleDistM = &vis

temp := float32(prediction.Data.AirTemperature)
cnd.TempC = &temp

symbolCode := int(prediction.Data.SymbolCode)
if condition, ok := weatherConditions[symbolCode]; ok {
cnd.Code = condition.WeatherCode
cnd.Desc = condition.Description
} else {
cnd.Code = iface.CodeUnknown
cnd.Desc = fmt.Sprintf("Unknown weather condition (symbol code %d)", symbolCode)
}

windSpeed := float32(prediction.Data.WindSpeed * 3.6) // convert m/s to km/h
cnd.WindspeedKmph = &windSpeed

gustSpeed := float32(prediction.Data.WindSpeedOfGust * 3.6) // convert m/s to km/h
cnd.WindGustKmph = &gustSpeed

wd := int(prediction.Data.WindFromDirection)
cnd.WinddirDegree = &wd

humidity := int(prediction.Data.RelativeHumidity)
cnd.Humidity = &humidity

return cnd
}

Expand Down