// SPDX-FileCopyrightText: 2023 Winni Neessen // // SPDX-License-Identifier: MIT package meteologix import ( "encoding/json" "fmt" ) // ErrUnsupportedDirection is returned when a direction degree is given, // that is not resolvable var ErrUnsupportedDirection = "Unsupported direction" // Observation represents the observation API response for a Station type Observation struct { // Altitude is the altitude of the station providing the Observation Altitude *int `json:"ele,omitempty"` // Data holds the different APIObservationData points Data APIObservationData `json:"data"` // Name is the name of the Station providing the Observation Name string `json:"name"` // Latitude represents the GeoLocation latitude coordinates for the Station Latitude float64 `json:"lat"` // Longitude represents the GeoLocation longitude coordinates for the Station Longitude float64 `json:"lon"` // StationID is the ID of the Station providing the Observation StationID string `json:"stationId"` } // APIObservationData holds the different data points of the Observation as // returned by the station observation API endpoints. // // Please keep in mind that different Station types return different values, therefore // all values are represented as pointer type returning nil if the data point in question // is not returned for the requested Station. type APIObservationData struct { // Dewpoint represents the dewpoint in °C Dewpoint *APIFloat `json:"dewpoint,omitempty"` // DewPointMean represents the mean dewpoint in °C DewpointMean *APIFloat `json:"dewpointMean,omitempty"` // GlobalRadiation10m represents the sum of global radiation over the last // 10 minutes in kJ/m² GlobalRadiation10m *APIFloat `json:"globalRadiation10m,omitempty"` // GlobalRadiation1h represents the sum of global radiation over the last // 1 hour in kJ/m² GlobalRadiation1h *APIFloat `json:"globalRadiation1h,omitempty"` // GlobalRadiation24h represents the sum of global radiation over the last // 24 hour in kJ/m² GlobalRadiation24h *APIFloat `json:"globalRadiation24h,omitempty"` // HumidityRelative represents the relative humidity in percent HumidityRelative *APIFloat `json:"humidityRelative,omitempty"` // Precipitation represents the current amount of precipitation Precipitation *APIFloat `json:"prec,omitempty"` // Precipitation10m represents the amount of precipitation over the last 10 minutes Precipitation10m *APIFloat `json:"prec10m,omitempty"` // Precipitation1h represents the amount of precipitation over the last hour Precipitation1h *APIFloat `json:"prec1h,omitempty"` // Precipitation24h represents the amount of precipitation over the last 24 hours Precipitation24h *APIFloat `json:"prec24h,omitempty"` // PressureMSL represents the air pressure at MSL / temperature adjusted (QFF) in hPa PressureMSL *APIFloat `json:"pressureMsl,omitempty"` // PressureQFE represents the pressure at station level (QFE) in hPa PressureQFE *APIFloat `json:"pressure,omitempty"` // Temperature represents the temperature in °C Temperature *APIFloat `json:"temp,omitempty"` // TemperatureMax represents the maximum temperature in °C TemperatureMax *APIFloat `json:"tempMax,omitempty"` // TemperatureMean represents the mean temperature in °C TemperatureMean *APIFloat `json:"tempMean,omitempty"` // TemperatureMin represents the minimum temperature in °C TemperatureMin *APIFloat `json:"tempMin,omitempty"` // Temperature5cm represents the temperature 5cm above ground in °C Temperature5cm *APIFloat `json:"temp5cm,omitempty"` // Temperature5cm represents the minimum temperature 5cm above // ground in °C Temperature5cmMin *APIFloat `json:"temp5cmMin,omitempty"` // WindDirection represents the direction from which the wind // originates in degree (0=N, 90=E, 180=S, 270=W) WindDirection *APIFloat `json:"windDirection,omitempty"` // WindSpeed represents the wind speed in knots (soon switched to m/s) WindSpeed *APIFloat `json:"windSpeed,omitempty"` } // ObservationLatestByStationID returns the latest Observation values from the // given Station func (c *Client) ObservationLatestByStationID(si string) (Observation, error) { var o Observation u := fmt.Sprintf("%s/station/%s/observations/latest", c.config.apiURL, si) r, err := c.httpClient.Get(u) if err != nil { return o, fmt.Errorf("API request failed: %w", err) } if err := json.Unmarshal(r, &o); err != nil { return o, fmt.Errorf("failed to unmarshal API response JSON: %w", err) } return o, nil } // ObservationLatestByLocation performs a GeoLocation lookup of the location string, checks for any // nearby weather stations (25 km radius) and returns the latest Observation values from the // Stations with the shortest distance. It will also return the Station that was used for the query. // It will throw an error if no station could be found in that queried location. func (c *Client) ObservationLatestByLocation(l string) (Observation, Station, error) { sl, err := c.StationSearchByLocationWithinRadius(l, 25) if err != nil { return Observation{}, Station{}, fmt.Errorf("failed search locations at given location: %w", err) } s := sl[0] o, err := c.ObservationLatestByStationID(s.ID) return o, s, err } // Dewpoint returns the dewpoint data point as Temperature // If the data point is not available in the Observation it will return // Temperature in which the "not available" field will be // true. func (o Observation) Dewpoint() Temperature { if o.Data.Dewpoint == nil { return Temperature{na: true} } return Temperature{ dt: o.Data.Dewpoint.DateTime, n: FieldDewpoint, s: SourceObservation, fv: o.Data.Dewpoint.Value, } } // DewpointMean returns the mean dewpoint data point as Temperature. // If the data point is not available in the Observation it will return // Temperature in which the "not available" field will be // true. func (o Observation) DewpointMean() Temperature { if o.Data.DewpointMean == nil { return Temperature{na: true} } return Temperature{ dt: o.Data.DewpointMean.DateTime, n: FieldDewpointMean, s: SourceObservation, fv: o.Data.DewpointMean.Value, } } // Temperature returns the temperature data point as Temperature. // If the data point is not available in the Observation it will return // Temperature in which the "not available" field will be // true. func (o Observation) Temperature() Temperature { if o.Data.Temperature == nil { return Temperature{na: true} } return Temperature{ dt: o.Data.Temperature.DateTime, n: FieldTemperature, s: SourceObservation, fv: o.Data.Temperature.Value, } } // TemperatureAtGround returns the temperature at ground level (5cm) // data point as Temperature. // If the data point is not available in the Observation it will return // Temperature in which the "not available" field will be // true. func (o Observation) TemperatureAtGround() Temperature { if o.Data.Temperature5cm == nil { return Temperature{na: true} } return Temperature{ dt: o.Data.Temperature5cm.DateTime, n: FieldTemperatureAtGround, s: SourceObservation, fv: o.Data.Temperature5cm.Value, } } // TemperatureMax returns the maximum temperature so far data point as // Temperature. // If the data point is not available in the Observation it will return // Temperature in which the "not available" field will be // true. func (o Observation) TemperatureMax() Temperature { if o.Data.TemperatureMax == nil { return Temperature{na: true} } return Temperature{ dt: o.Data.TemperatureMax.DateTime, n: FieldTemperatureMax, s: SourceObservation, fv: o.Data.TemperatureMax.Value, } } // TemperatureMin returns the minimum temperature so far data point as // Temperature. // If the data point is not available in the Observation it will return // Temperature in which the "not available" field will be // true. func (o Observation) TemperatureMin() Temperature { if o.Data.TemperatureMin == nil { return Temperature{na: true} } return Temperature{ dt: o.Data.TemperatureMin.DateTime, n: FieldTemperatureMin, s: SourceObservation, fv: o.Data.TemperatureMin.Value, } } // TemperatureAtGroundMin returns the minimum temperature so far // at ground level (5cm) data point as Temperature // If the data point is not available in the Observation it will return // Temperature in which the "not available" field will be // true. func (o Observation) TemperatureAtGroundMin() Temperature { if o.Data.Temperature5cmMin == nil { return Temperature{na: true} } return Temperature{ dt: o.Data.Temperature5cmMin.DateTime, n: FieldTemperatureAtGroundMin, s: SourceObservation, fv: o.Data.Temperature5cmMin.Value, } } // TemperatureMean returns the mean temperature data point as Temperature. // If the data point is not available in the Observation it will return // Temperature in which the "not available" field will be // true. func (o Observation) TemperatureMean() Temperature { if o.Data.TemperatureMean == nil { return Temperature{na: true} } return Temperature{ dt: o.Data.TemperatureMean.DateTime, n: FieldTemperatureMean, s: SourceObservation, fv: o.Data.TemperatureMean.Value, } } // HumidityRelative returns the relative humidity data point as float64. // If the data point is not available in the Observation it will return // Humidity in which the "not available" field will be // true. func (o Observation) HumidityRelative() Humidity { if o.Data.HumidityRelative == nil { return Humidity{na: true} } return Humidity{ dt: o.Data.HumidityRelative.DateTime, n: FieldHumidityRelative, s: SourceObservation, fv: o.Data.HumidityRelative.Value, } } // PressureMSL returns the relative pressure at mean seal level data point // as Pressure. // If the data point is not available in the Observation it will return // Pressure in which the "not available" field will be // true. func (o Observation) PressureMSL() Pressure { if o.Data.PressureMSL == nil { return Pressure{na: true} } return Pressure{ dt: o.Data.PressureMSL.DateTime, n: FieldPressureMSL, s: SourceObservation, fv: o.Data.PressureMSL.Value, } } // PressureQFE returns the relative pressure at mean seal level data point // as Pressure. // If the data point is not available in the Observation it will return // Pressure in which the "not available" field will be // true. func (o Observation) PressureQFE() Pressure { if o.Data.PressureQFE == nil { return Pressure{na: true} } return Pressure{ dt: o.Data.PressureQFE.DateTime, n: FieldPressureQFE, s: SourceObservation, fv: o.Data.PressureQFE.Value, } } // Precipitation returns the current amount of precipitation (mm) as // Precipitation // If the data point is not available in the Observation it will return // Precipitation in which the "not available" field will be // true. func (o Observation) Precipitation(ts Timespan) Precipitation { var df *APIFloat var fn Fieldname switch ts { case TimespanCurrent: df = o.Data.Precipitation fn = FieldPrecipitation case Timespan10Min: df = o.Data.Precipitation10m fn = FieldPrecipitation10m case Timespan1Hour: df = o.Data.Precipitation1h fn = FieldPrecipitation1h case Timespan24Hours: df = o.Data.Precipitation24h fn = FieldPrecipitation24h default: return Precipitation{na: true} } if df == nil { return Precipitation{na: true} } return Precipitation{ dt: df.DateTime, n: fn, s: SourceObservation, fv: df.Value, } } // GlobalRadiation returns the current amount of global radiation as // Radiation // If the data point is not available in the Observation it will return // Radiation in which the "not available" field will be // true. func (o Observation) GlobalRadiation(ts Timespan) Radiation { var df *APIFloat var fn Fieldname switch ts { case Timespan10Min: df = o.Data.GlobalRadiation10m fn = FieldGlobalRadiation10m case Timespan1Hour: df = o.Data.GlobalRadiation1h fn = FieldGlobalRadiation1h case Timespan24Hours: df = o.Data.GlobalRadiation24h fn = FieldGlobalRadiation24h default: return Radiation{na: true} } if df == nil { return Radiation{na: true} } return Radiation{ dt: df.DateTime, n: fn, s: SourceObservation, fv: df.Value, } } // WindDirection returns the current direction from which the wind // originates in degree (0=N, 90=E, 180=S, 270=W) as Direction. // If the data point is not available in the Observation it will return // Direction in which the "not available" field will be true. func (o Observation) WindDirection() Direction { if o.Data.WindDirection == nil { return Direction{na: true} } return Direction{ dt: o.Data.WindDirection.DateTime, n: FieldWindDirection, s: SourceObservation, fv: o.Data.WindDirection.Value, } } // WindSpeed returns the current windspeed data point as Speed. // If the data point is not available in the Observation it will return // Speed in which the "not available" field will be true. func (o Observation) WindSpeed() Speed { if o.Data.WindSpeed == nil { return Speed{na: true} } return Speed{ dt: o.Data.WindSpeed.DateTime, n: FieldWindSpeed, s: SourceObservation, fv: o.Data.WindSpeed.Value * 0.5144444444, } }