summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2026-02-01 13:24:19 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2026-02-01 13:24:19 +0200
commitc9cc45a8c97496a3996df968360f096cf7172407 (patch)
tree7fb09bd4f2354f357009b9d20a4c45f568c30ff0
downloadweather-c9cc45a8c97496a3996df968360f096cf7172407.tar.zst
Initial
-rw-r--r--main.go417
1 files changed, 417 insertions, 0 deletions
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..b761844
--- /dev/null
+++ b/main.go
@@ -0,0 +1,417 @@
+package main
+
+import (
+ "encoding/json"
+ "encoding/xml"
+ "flag"
+ "fmt"
+ "io"
+ "math"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type FeatureCollection struct {
+ XMLName xml.Name `xml:"http://www.opengis.net/wfs/2.0 FeatureCollection"`
+ Member []Member `xml:"http://www.opengis.net/wfs/2.0 member"`
+}
+
+type Member struct {
+ Observation Observation `xml:"http://www.opengis.net/om/2.0 OM_Observation"`
+}
+
+type Observation struct {
+ Result Result `xml:"http://www.opengis.net/om/2.0 result"`
+}
+
+type Result struct {
+ MultiPointCoverage MultiPointCoverage `xml:"http://www.opengis.net/gmlcov/1.0 MultiPointCoverage"`
+}
+
+type MultiPointCoverage struct {
+ DomainSet DomainSet `xml:"http://www.opengis.net/gml/3.2 domainSet"`
+ RangeSet RangeSet `xml:"http://www.opengis.net/gml/3.2 rangeSet"`
+ RangeType RangeType `xml:"http://www.opengis.net/gmlcov/1.0 rangeType"`
+}
+
+type DomainSet struct {
+ MultiPoint MultiPoint `xml:"http://www.opengis.net/gml/3.2 MultiPoint"`
+}
+
+type MultiPoint struct {
+ PosList string `xml:"http://www.opengis.net/gml/3.2 posList"`
+}
+
+type RangeSet struct {
+ DataBlock DataBlock `xml:"http://www.opengis.net/gml/3.2 DataBlock"`
+}
+
+type DataBlock struct {
+ TupleList string `xml:"http://www.opengis.net/gml/3.2 doubleOrNilReasonTupleList"`
+}
+
+type RangeType struct {
+ DataRecord DataRecord `xml:"http://www.opengis.net/swe/2.0 DataRecord"`
+}
+
+type DataRecord struct {
+ Field []Field `xml:"http://www.opengis.net/swe/2.0 field"`
+}
+
+type Field struct {
+ Name string `xml:"name,attr"`
+}
+
+type OWMResponse struct {
+ Lat float64 `json:"lat"`
+ Lon float64 `json:"lon"`
+ Timezone string `json:"timezone"`
+ TimezoneOffset int `json:"timezone_offset"`
+ Current Current `json:"current"`
+ Hourly []Current `json:"hourly"`
+}
+
+type Current struct {
+ Dt int64 `json:"dt"`
+ Sunrise int64 `json:"sunrise,omitempty"`
+ Sunset int64 `json:"sunset,omitempty"`
+ Temp float64 `json:"temp"`
+ FeelsLike float64 `json:"feels_like"`
+ Pressure int `json:"pressure"`
+ Humidity int `json:"humidity"`
+ DewPoint float64 `json:"dew_point"`
+ Uvi float64 `json:"uvi"`
+ Clouds int `json:"clouds"`
+ Visibility int `json:"visibility"`
+ WindSpeed float64 `json:"wind_speed"`
+ WindDeg int `json:"wind_deg"`
+ WindGust float64 `json:"wind_gust"`
+ Weather []Weather `json:"weather"`
+ Rain *Rain `json:"rain,omitempty"`
+}
+
+type Rain struct {
+ OneH float64 `json:"1h"`
+}
+
+type Weather struct {
+ ID int `json:"id"`
+ Main string `json:"main"`
+ Description string `json:"description"`
+ Icon string `json:"icon"`
+}
+
+// Parameters for the sunrise and sunset calculation
+type Parameters struct {
+ Latitude float64
+ Longitude float64
+ UtcOffset float64
+ Date time.Time
+}
+
+// GetSunriseSunset calculates the sunrise and sunset times
+func (p Parameters) GetSunriseSunset() (time.Time, time.Time, error) {
+ // Convert the date to julian day
+ julianDay := p.DateToJulianDay()
+
+ // Calculate the julian century
+ julianCentury := (julianDay - 2451545) / 36525.0
+
+ // Geom mean long sun (deg)
+ geomMeanLongSun := math.Mod(280.46646+julianCentury*(36000.76983+julianCentury*0.0003032), 360)
+
+ // Geom mean anom sun (deg)
+ geomMeanAnomSun := 357.52911 + julianCentury*(35999.05029-0.0001537*julianCentury)
+
+ // Eccent earth orbit
+ eccentEarthOrbit := 0.016708634 - julianCentury*(0.000042037+0.0000001267*julianCentury)
+
+ // Sun eq of ctr
+ sunEqOfCtr := math.Sin(degToRad(geomMeanAnomSun))*(1.914602-julianCentury*(0.004817+0.000014*julianCentury)) +
+ math.Sin(degToRad(2*geomMeanAnomSun))*(0.019993-0.000101*julianCentury) +
+ math.Sin(degToRad(3*geomMeanAnomSun))*0.000289
+
+ // Sun true long (deg)
+ sunTrueLong := geomMeanLongSun + sunEqOfCtr
+
+ // Sun app long (deg)
+ sunAppLong := sunTrueLong - 0.00569 - 0.00478*math.Sin(degToRad(125.04-1934.136*julianCentury))
+
+ // Mean obliq ecliptic (deg)
+ meanObliqEcliptic := 23 + (26+(21.448-julianCentury*(46.815+julianCentury*(0.00059-julianCentury*0.001813)))/60)/60
+
+ // Obliq corr (deg)
+ obliqCorr := meanObliqEcliptic + 0.00256*math.Cos(degToRad(125.04-1934.136*julianCentury))
+
+ // Sun declin (deg)
+ sunDeclin := radToDeg(math.Asin(math.Sin(degToRad(obliqCorr)) * math.Sin(degToRad(sunAppLong))))
+
+ // Var y
+ varY := math.Tan(degToRad(obliqCorr/2)) * math.Tan(degToRad(obliqCorr/2))
+
+ // Eq of time (minutes)
+ eqOfTime := 4 * radToDeg(varY*math.Sin(2*degToRad(geomMeanLongSun))-
+ 2*eccentEarthOrbit*math.Sin(degToRad(geomMeanAnomSun))+
+ 4*eccentEarthOrbit*varY*math.Sin(degToRad(geomMeanAnomSun))*math.Cos(2*degToRad(geomMeanLongSun))-
+ 0.5*varY*varY*math.Sin(4*degToRad(geomMeanLongSun))-
+ 1.25*eccentEarthOrbit*eccentEarthOrbit*math.Sin(2*degToRad(geomMeanAnomSun)))
+
+ // HA sunrise (deg)
+ haSunrise := radToDeg(math.Acos(math.Cos(degToRad(90.833))/(math.Cos(degToRad(p.Latitude))*math.Cos(degToRad(sunDeclin))) - math.Tan(degToRad(p.Latitude))*math.Tan(degToRad(sunDeclin))))
+
+ // Solar noon (LST)
+ solarNoon := (720 - 4*p.Longitude - eqOfTime + p.UtcOffset*60) / 1440
+
+ // Sunrise time (LST)
+ sunriseTime := solarNoon - haSunrise*4/1440
+
+ // Sunset time (LST)
+ sunsetTime := solarNoon + haSunrise*4/1440
+
+ // Convert the sunrise and sunset to time.Time
+ sunrise := p.julianDayToDate(julianDay + sunriseTime)
+ sunset := p.julianDayToDate(julianDay + sunsetTime)
+
+ return sunrise, sunset, nil
+}
+
+// DateToJulianDay converts a date to julian day
+func (p Parameters) DateToJulianDay() float64 {
+ year := float64(p.Date.Year())
+ month := float64(p.Date.Month())
+ day := float64(p.Date.Day())
+
+ if month <= 2 {
+ year -= 1
+ month += 12
+ }
+
+ a := math.Floor(year / 100)
+ b := 2 - a + math.Floor(a/4)
+
+ return math.Floor(365.25*(year+4716)) + math.Floor(30.6001*(month+1)) + day + b - 1524.5
+}
+
+// julianDayToDate converts a julian day to date
+func (p Parameters) julianDayToDate(julianDay float64) time.Time {
+ julianDay += 0.5
+ z := math.Floor(julianDay)
+ f := julianDay - z
+
+ var a float64
+ if z >= 2299161 {
+ alpha := math.Floor((z - 1867216.25) / 36524.25)
+ a = z + 1 + alpha - math.Floor(alpha/4)
+ } else {
+ a = z
+ }
+
+ b := a + 1524
+ c := math.Floor((b - 122.1) / 365.25)
+ d := math.Floor(365.25 * c)
+ e := math.Floor((b - d) / 30.6001)
+
+ day := b - d - math.Floor(30.6001*e) + f
+ month := e - 1
+ if month > 12 {
+ month -= 12
+ }
+ year := c - 4715
+ if month > 2 {
+ year -= 1
+ }
+
+ hour := int(math.Floor((day - math.Floor(day)) * 24))
+ minute := int(math.Floor(((day-math.Floor(day))*24 - float64(hour)) * 60))
+ second := int(math.Floor((((day-math.Floor(day))*24-float64(hour))*60 - float64(minute)) * 60))
+
+ return time.Date(int(year), time.Month(month), int(math.Floor(day)), hour, minute, second, 0, time.UTC).Add(time.Hour * time.Duration(p.UtcOffset))
+}
+
+func degToRad(deg float64) float64 {
+ return deg * math.Pi / 180
+}
+
+func radToDeg(rad float64) float64 {
+ return rad * 180 / math.Pi
+}
+
+// Map FMI WeatherSymbol3 to OWM weather
+var fmiToOwm = map[int]Weather{
+ 1: {800, "Clear", "clear sky", "01"},
+ 2: {801, "Clouds", "few clouds", "02"},
+ 3: {802, "Clouds", "scattered clouds", "03"},
+ 4: {803, "Clouds", "broken clouds", "04"},
+ 5: {804, "Clouds", "overcast clouds", "04"},
+ // Add more mappings as needed, e.g.
+ 21: {520, "Rain", "light intensity shower rain", "09"},
+ 22: {521, "Rain", "shower rain", "09"},
+ 23: {522, "Rain", "heavy intensity shower rain", "09"},
+ 30: {500, "Rain", "light rain", "10"},
+ 31: {501, "Rain", "moderate rain", "10"},
+ 32: {502, "Rain", "heavy intensity rain", "10"},
+ // Snow
+ 41: {600, "Snow", "light snow", "13"},
+ // Thunder
+ 91: {200, "Thunderstorm", "thunderstorm with light rain", "11"},
+ // etc. Expand based on full list
+}
+
+// CalculateFeelsLike simple apparent temperature
+func calculateFeelsLike(temp, humidity float64, windSpeed float64) float64 {
+ e := humidity / 100 * 6.105 * math.Exp(17.27*temp/(237.7+temp))
+ return temp + 0.33*e - 0.70*windSpeed - 4.00
+}
+
+func main() {
+ location := flag.String("place", "Helsinki", "Location for the forecast")
+ flag.Parse()
+
+ url := fmt.Sprintf("https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::harmonie::surface::point::multipointcoverage&place=%s&parameters=Temperature,PrecipitationAmount,Humidity,Pressure,DewPointTemperature,WindSpeedMS,WindDirection,WindGust,TotalCloudCover,HorizontalVisibility,WeatherSymbol3", *location)
+
+ resp, err := http.Get(url)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error fetching data: %v\n", err)
+ os.Exit(1)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error reading response: %v\n", err)
+ os.Exit(1)
+ }
+
+ var fc FeatureCollection
+ err = xml.Unmarshal(body, &fc)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error parsing XML: %v\n", err)
+ os.Exit(1)
+ }
+
+ if len(fc.Member) == 0 {
+ fmt.Fprintf(os.Stderr, "No data found\n")
+ os.Exit(1)
+ }
+
+ mpc := fc.Member[0].Observation.Result.MultiPointCoverage
+
+ pos := strings.Fields(mpc.DomainSet.MultiPoint.PosList)
+ if len(pos) < 3 {
+ fmt.Fprintf(os.Stderr, "Invalid posList\n")
+ os.Exit(1)
+ }
+
+ lat, _ := strconv.ParseFloat(pos[0], 64)
+ lon, _ := strconv.ParseFloat(pos[1], 64)
+
+ var times []int64
+ for i := 2; i < len(pos); i += 3 {
+ t, _ := strconv.ParseInt(pos[i], 10, 64)
+ times = append(times, t)
+ }
+
+ params := []string{}
+ for _, f := range mpc.RangeType.DataRecord.Field {
+ params = append(params, f.Name)
+ }
+
+ paramIndex := make(map[string]int)
+ for i, p := range params {
+ paramIndex[p] = i
+ }
+
+ valsStr := strings.Fields(strings.TrimSpace(mpc.RangeSet.DataBlock.TupleList))
+ numPoints := len(times)
+ numParams := len(params)
+ if len(valsStr) != numPoints*numParams {
+ fmt.Fprintf(os.Stderr, "Data mismatch\n")
+ os.Exit(1)
+ }
+
+ var values [][]float64
+ for i := 0; i < numPoints; i++ {
+ row := make([]float64, numParams)
+ for j := 0; j < numParams; j++ {
+ row[j], _ = strconv.ParseFloat(valsStr[i*numParams+j], 64)
+ }
+ values = append(values, row)
+ }
+
+ loc, _ := time.LoadLocation("Europe/Helsinki")
+ firstDt := time.Unix(times[0], 0).UTC()
+ _, offset := firstDt.In(loc).Zone()
+
+ var response OWMResponse
+ response.Lat = lat
+ response.Lon = lon
+ response.Timezone = "Europe/Helsinki"
+ response.TimezoneOffset = offset
+
+ // Calculate sunrise/sunset for the day of first forecast
+ p := Parameters{
+ Latitude: lat,
+ Longitude: lon,
+ UtcOffset: float64(offset) / 3600,
+ Date: firstDt,
+ }
+ sunrise, sunset, _ := p.GetSunriseSunset()
+ response.Current.Sunrise = sunrise.Unix()
+ response.Current.Sunset = sunset.Unix()
+
+ var hourly []Current
+ for i := 0; i < numPoints; i++ {
+ row := values[i]
+ dt := times[i]
+ var curr Current
+ curr.Dt = dt
+ curr.Temp = row[paramIndex["Temperature"]]
+ curr.Humidity = int(row[paramIndex["Humidity"]])
+ curr.Pressure = int(row[paramIndex["Pressure"]])
+ curr.DewPoint = row[paramIndex["DewPointTemperature"]]
+ curr.WindSpeed = row[paramIndex["WindSpeedMS"]]
+ curr.WindDeg = int(row[paramIndex["WindDirection"]])
+ curr.WindGust = row[paramIndex["WindGust"]]
+ curr.Clouds = int(row[paramIndex["TotalCloudCover"]])
+ curr.Visibility = int(row[paramIndex["HorizontalVisibility"]])
+ curr.Uvi = 0 // Not available
+ curr.FeelsLike = calculateFeelsLike(curr.Temp, float64(curr.Humidity), curr.WindSpeed)
+
+ precip := row[paramIndex["PrecipitationAmount"]]
+ if precip > 0 {
+ curr.Rain = &Rain{OneH: precip}
+ }
+
+ symbol := int(row[paramIndex["WeatherSymbol3"]])
+ w, ok := fmiToOwm[symbol]
+ if !ok {
+ w = Weather{741, "Fog", "unknown", "50"} // Default
+ }
+
+ // Day or night
+ t := time.Unix(dt, 0).UTC()
+ isDay := t.After(sunrise) && t.Before(sunset)
+ if isDay {
+ w.Icon += "d"
+ } else {
+ w.Icon += "n"
+ }
+ curr.Weather = []Weather{w}
+
+ hourly = append(hourly, curr)
+ }
+
+ response.Current = hourly[0]
+ response.Hourly = hourly
+
+ jsonData, err := json.Marshal(response)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error marshaling JSON: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Println(string(jsonData))
+}