csparpa / pyowm

A Python wrapper around the OpenWeatherMap web API
https://pyowm.readthedocs.io
MIT License
789 stars 171 forks source link

How To Retrieve Weather Forecast From OpenWeatherMap For A Given Local Date and Time expressed using Local Time Zone? #382

Closed dcs3spp closed 2 years ago

dcs3spp commented 3 years ago

OpenWeatherMap API provides a free 5 day weather forecast service with forecast reports at 3 hour intervals per day for a given city.

daily_forecast_data = [UTC date:12:00, UTC date:03:00, UTC date:06:00, UTC date:09:00, UTC date:12:00, UTC date:15:00, UTC date:18:00, UTC date:21:00]

If the requester is in Sydney the UTC time offset is +10:00 hrs.

So assuming the following 5 day forecast time window:

start = 2021-09-03T12:00:00:00:00
end = 2021-09-08T12:00:00:00:00

If I am providing an API endpoint weather server that accepts a city and ISO8601 date string for that city's local timezone, how do I retrieve the correct weather forecast from OpenWeatherMap for that date and time for that city?

API Sydney Example GET /weather/sydney/?when=2021-09-06T15:00+10:00

This represents querying a forecast for 2021-09-06 3pm in Sydney.

The OpenWeatherMap 5 day forecast for a city gives a response with 3hour reference timestamps reported in UTC for those 5 days.

Would I have to filter the openweathermap resultset for 2021-09-06 and then convert each 3 hour slot's, reference timestamp to local time to find the closest match to T15:00+10:00?

I have tried using pyorm Python library to facilitate making the request to openweathermap.org:

    owm = OWM("<API KEY>")
    mgr = owm.weather_manager()
    three_h_forecaster = mgr.forecast_at_place("Sydney,AUS", "3h")
    day_iso = "2021-09-06T15:00+10:00:00"

    weather = three_h_forecaster.get_weather_at(day_iso)
    print(f"WEATHER FOR Sydney 3H starts := {three_h_forecaster.when_starts('iso')}")
    print(f"WEATHER FOR Sydney 3H ends := {three_h_forecaster.when_ends('iso')}")
    print(f"Weather for {day_iso} is :: \n{json.dumps(weather.to_dict(), indent=4)}")
    print(f"Weather reference time is: {weather.reference_time('iso')}")
WEATHER FOR Sydney 3H starts := 2021-09-03 15:00:00+00:00
WEATHER FOR Sydney 3H ends := 2021-09-08 12:00:00+00:00
Weather for 2021-09-06T15:00+10:00:00 is :: 
{
    "reference_time": 1630908000,
    "sunset_time": null,
    "sunrise_time": null,
    "clouds": 32,
    "rain": {},
    "snow": {},
    "wind": {
        "speed": 10.42,
        "deg": 193,
        "gust": 14.76
    },
    "humidity": 53,
    "pressure": {
        "press": 1027,
        "sea_level": 1027
    },
    "temperature": {
        "temp": 287.64,
        "temp_kf": 0,
        "temp_max": 287.64,
        "temp_min": 287.64,
        "feels_like": 286.53
    },
    "status": "Clouds",
    "detailed_status": "scattered clouds",
    "weather_code": 802,
    "weather_icon_name": "03d",
    "visibility_distance": 10000,
    "dewpoint": null,
    "humidex": null,
    "heat_index": null,
    "utc_offset": null,
    "uvi": null,
    "precipitation_probability": 0.02
}
Weather reference time is: 2021-09-06 06:00:00+00:00

Am I using this correct? I notice that the reference time reported is 2021-09-06 06:00:00+00:00....

csparpa commented 2 years ago

Hi @dcs3spp I would advice to use the OneCall features so that you get fresh data and no backwards-compatibility issues in the long run . And bonus feature: you get forecasted weather with HOURLY granularity (you're aiming for 3 hours now)

Apart from that, I guess the correct process is:

  1. you accept local time as a parameter (eg. Sidney timezone) to your endpoint
  2. you turn that into UTC and extract the related UNIX epoch
  3. you can pass that UNIX epoch to the utility function: pyowm.utils.weather.find_closest_weather

This is a working example that retrieves forecasted weather on Sidney at 15 hours after you run the code:

from pyowm.owm import OWM
from pyowm.utils.weather import find_closest_weather
from datetime import datetime, timedelta

owm = OWM('YOUR_API_KEY')

gcm = owm.geocoding_manager()
mgr = owm.weather_manager()

# This should be the epoch resulting from turning your endpoint's parameter to UTC...
target_time = int((datetime.utcnow() + timedelta(hours=15)).timestamp())  # UTC epoch for: now + 15 hours

# geocode Sidney
sidney = gcm.geocode('Sidney', 'AU')[0]

# call the API
response = mgr.one_call(lat=sidney.lat, lon=sidney.lon)  # OneCall object containing current and forecast weather on Sidney

# retrieve the forecasted weather that is closest to your target epoch
weathers = response.forecast_hourly
closest_weather = find_closest_weather(weathers, target_time)  # this is what you're looking for!

Is this of any help ?

dcs3spp commented 2 years ago

Hi @csparpa,

Yes!!! Many thanks, that is exactly what I want, thank you. Yep, this is a more efficient solution converting the local time to UTC for making request to openweathermap.org.

Also many thanks for the advice regarding using the OneCall API. That indeed looks more accurate providing a finer resolution of 1 hour with forecasts up to 7 days ahead.

Many thanks again @csparpa, appreciated :)

dcs3spp commented 2 years ago

Hi @csparpa ,

That worked great, thank-you, appreciated :)

I also need to add an additional endpoint, GET /weather/sydney/?when=2021-09-06 that allows the requester to retrieve the weather forecast for a specific future day.

In the example below I take an ISO8601 date, e.g. 2021-09-06, and city. Given the city, how can I determine what offset to apply?

For example, for Niue in New Zealand I would have to add on 11 hours to convert to UTC. Alternatively, if somewhere like Fiji I would have to subtract 12 hours to convert to UTC, which would make the date 2021-09-05.....

Would I have to make an initial request using pyowm and get the timezone offset from the JSON data and then make another request with the correct timestamp?

 def for_day(self, city: str, iso_date: str): 

        """ iso date param is iso formated date string, without time, e.g. 2021-09-06 """

        target_date = datetime.fromisoformat(iso_date).timestamp()

        try:
            city_coords = self._gcm.geocode(city)[0]

            response = self._mgr.one_call(
                exclude="minutely,hourly", lat=city_coords.lat, lon=city_coords.lon
            )   

        except UnauthorizedError:
            raise APIException("API key invalid", "internal server error", 500)

        weathers = response.forecast_daily
        closest_weather = find_closest_weather(weathers, target_date)

        return closest_weather
csparpa commented 2 years ago

Hey @dcs3spp I think your code is doing it right! Only one call is needed to the OWM API, and it's already in there.

FYI if you're returning a JSON blob to your HTTP endpoint callers, keep in mind that PyOWM class instances can be dumped to Python dictionaries, which in turn can be easily JSON-ified. For instance:

import json

# [...]

weathers = response.forecast_daily
closest_weather = find_closest_weather(weathers, target_date)

json_string = json.dumps(closest_weather.to_dict())
dcs3spp commented 2 years ago

Ok, many thanks @csparpa.

I was just not sure of which timestamp to use. The code I have for the additional endpoint uses a naive only date string which would use the local timestamp of the API server., i.e.

target_date = datetime.fromisoformat(iso_date).timestamp()

Is there anyway to lookup the timezone for the a city requested or is this not needed for the daily forecast aspect?

csparpa commented 2 years ago

Normally you don't need to specify any timezone when you invoke the OneCall API for a city

As far as I konw, OWM does not provide any timezone detail when looking up for city IDs or geocoding

Nevertheless, Python libraries do exist for this purpose (eg: geopy)