commit 4db6747e3e164912c97806e4d8e3126a27856d72 Author: Knut Ahlers Date: Thu May 19 19:12:00 2016 +0200 Implemented fetching and parsing of METAR data diff --git a/go_metar_suite_test.go b/go_metar_suite_test.go new file mode 100644 index 0000000..0ba9960 --- /dev/null +++ b/go_metar_suite_test.go @@ -0,0 +1,13 @@ +package metar_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestGoMetar(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "GoMetar Suite") +} diff --git a/metar.go b/metar.go new file mode 100644 index 0000000..ee79a02 --- /dev/null +++ b/metar.go @@ -0,0 +1,129 @@ +package metar + +import ( + "encoding/xml" + "errors" + "fmt" + "net/http" + "time" +) + +const ( + apiSource = "https://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&stationString=%s&hoursBeforeNow=2&mostRecent=true" +) + +var ( + // HTTPClient is used to make requests, you can insert your own + HTTPClient = http.DefaultClient +) + +// Result holds all the data from the METAR request +type Result struct { + XMLName xml.Name `xml:"METAR"` + RawText string `xml:"raw_text"` // The raw METAR + StationID string `xml:"station_id"` // Station identifier; Always a four character alphanumeric( A-Z, 0-9) + ObservationTime time.Time `xml:"observation_time"` // Time this METAR was observed + Latitude float64 `xml:"latitude"` // The latitude (in decimal degrees) of the station that reported this METAR + Longitude float64 `xml:"longitude"` // The longitude (in decimal degrees) of the station that reported this METAR + Temperature float64 `xml:"temp_c"` // Air temperature (celsius) + Dewpoint float64 `xml:"dewpoint_c"` // Dewpoint temperature (celsius) + WindDirDegrees int64 `xml:"wind_dir_degrees"` // Direction from which the wind is blowing. 0 degrees=variable wind direction. + WindSpeed int64 `xml:"wind_speed_kt"` // Wind speed; 0 degree wdir and 0 wspd = calm winds (kts) + WindGust int64 `xml:"wind_gust_kt"` // Wind gust + VisibilityStatute float64 `xml:"visibility_statute_mi"` // Horizontal visibility (statute miles) + Altimeter float64 `xml:"altim_in_hg"` // Altimeter (inches of Hg) + SeaLevelPressure float64 `xml:"sea_level_pressure_mb"` // Sea-level pressure (mb) + QualityControlFlags QualityControlFlags `xml:"quality_control_flags"` // Quality control flags provide useful information about the METAR station(s) that provide the data. + WXString string `xml:"wx_string"` // WX string descriptions (https://www.aviationweather.gov/static/adds/docs/metars/wxSymbols_anno2.pdf) + SkyCondition struct { + SkyCover SkyCover `xml:"sky_cover,attr"` // Sky cover, up to four levels of sky cover can be reported ; OVX present when vert_vis_ft is reported + } `xml:"sky_condition"` + FlightCategory FlightCategory `xml:"flight_category"` // Flight category of this METAR + // Fields 19 to 29 currently not implemented + MetarType string `xml:"metar_type"` // METAR or SPECI + Elevation float64 `xml:"elevation_m"` // The elevation of the station that reported this METAR (meters) +} + +// QualityControlFlags provide useful information about the METAR station(s) that provide the data. +type QualityControlFlags struct { + XMLName xml.Name `xml:"quality_control_flags"` + NoSignal bool `xml:"no_signal"` +} + +// SkyCover defines and explains possible sky coverage situations +type SkyCover string + +// Common SkyCover situations +const ( + SkyCoverSKC SkyCover = "SKC" // "No cloud/Sky clear" used worldwide but in North America is used to indicate a human generated report + SkyCoverCLR SkyCover = "CLR" // "No clouds below 12,000 ft (3,700 m) (U.S.) or 25,000 ft (7,600 m) (Canada)", used mainly within North America and indicates a station that is at least partly automated + SkyCoverNSC SkyCover = "NSC" // "No (nil) significant cloud", i.e., none below 5,000 ft (1,500 m) and no TCU or CB. Not used in North America. + SkyCoverFEW SkyCover = "FEW" // "Few" = 1–2 oktas + SkyCoverSCT SkyCover = "SCT" // "Scattered" = 3–4 oktas + SkyCoverBKN SkyCover = "BKN" // "Broken" = 5–7 oktas + SkyCoverOVC SkyCover = "OVC" // "Overcast" = 8 oktas, i.e., full cloud coverage + SkyCoverCAVOK SkyCover = "CAVOK" // Ceiling And Visibility OKay, indicating no cloud below 5,000 ft (1,500 m) or the highest minimum sector altitude and no cumulonimbus or towering cumulus at any level, a visibility of 10 km (6 mi) or more and no significant weather change +) + +// FlightCategory defines and explains possible flight category situations +type FlightCategory string + +// Common FlightCategory situations +const ( + FlightCategoryVFR FlightCategory = "VFR" // Visual Flight Rules (Ceiling greater than 3,000 feet AGL and visibility greater than 5 miles) + FlightCategoryMVFR FlightCategory = "MVFR" // Marginal Visual Flight Rules (Ceiling 1,000 to 3,000 feet AGL and/or visibility 3 to 5 miles) + FlightCategoryIFR FlightCategory = "IFR" // Instrument Flight Rules (Ceiling 500 to below 1,000 feet AGL and/or visibility 1 mile to less than 3 miles) + FlightCategoryLIFR FlightCategory = "LIFR" // Low Instrument Flight Rules (Ceiling below 500 feet AGL and/or visibility less than 1 mile) +) + +type response struct { + XMLName xml.Name `xml:"response"` + Data struct { + NumResults int `xml:"num_results,attr"` + Results []Result `xml:"METAR"` + } `xml:"data"` +} + +// FetchCurrentStationWeather fetches the last result from the specified station if it was reported during last 2 hours +func FetchCurrentStationWeather(station string) (*Result, error) { + req, _ := http.NewRequest("GET", fmt.Sprintf(apiSource, station), nil) + res, err := HTTPClient.Do(req) + if err != nil { + return nil, err + } + + r := &response{} + if err = xml.NewDecoder(res.Body).Decode(r); err != nil { + return nil, err + } + + if r.Data.NumResults != len(r.Data.Results) { + return nil, errors.New("Got inconsistent number of results") + } + + if r.Data.NumResults == 0 { + return nil, errors.New("Did not find any data for your station") + } + + return &r.Data.Results[0], nil +} + +// InHgTohPa converts "inch of mercury" to "hectopascal" +func InHgTohPa(inHg float64) float64 { + return inHg * 33.8638866667 +} + +// KtsToMs converts "knots" to "meters per second" +func KtsToMs(kts float64) float64 { + return kts * 0.514444 +} + +// StatMileToKm converts "statute miles" to "kilometers" +func StatMileToKm(sm float64) float64 { + return sm * 1.60934 +} + +// MbTohPa converts "millibar" to "hectopascal" +func MbTohPa(mb float64) float64 { + return mb * 0.1 +} diff --git a/metar_test.go b/metar_test.go new file mode 100644 index 0000000..97df7d1 --- /dev/null +++ b/metar_test.go @@ -0,0 +1,51 @@ +package metar_test + +import ( + "time" + + . "github.com/Luzifer/go-metar" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Metar", func() { + var ( + station = "" + result *Result + err error + ) + + JustBeforeEach(func() { + result, err = FetchCurrentStationWeather(station) + }) + + Context("with station EDDH (HAM)", func() { + BeforeEach(func() { + station = "EDDH" + }) + + It("should not have errored", func() { + Expect(err).NotTo(HaveOccurred()) + }) + + It("should be at the expected position", func() { + Expect(result.Latitude).To(Equal(53.63)) + Expect(result.Longitude).To(Equal(10.0)) + }) + + It("should be a METAR station reporting", func() { + Expect(result.MetarType).To(Equal("METAR")) + }) + + It("should be a fairly new result", func() { + Expect(time.Since(result.ObservationTime) < 1*time.Hour).To(BeTrue()) + }) + + It("should have information about SkyCover and FlightCategory", func() { + Expect(result.SkyCondition.SkyCover).NotTo(Equal(SkyCover(""))) + Expect(result.FlightCategory).NotTo(Equal(FlightCategory(""))) + }) + }) + +})