API - refacto weather utils

This commit is contained in:
Sam
2022-11-17 00:13:43 +01:00
parent 64d770a016
commit 940f0a8416
10 changed files with 581 additions and 141 deletions

View File

@ -0,0 +1,5 @@
from .weather_service import WeatherService
__all__ = [
'WeatherService',
]

View File

@ -0,0 +1,50 @@
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Dict, Optional
from gpxpy.gpx import GPXTrackPoint
class BaseWeather(ABC):
def __init__(self, api_key: str) -> None:
self.api_key: str = api_key
@abstractmethod
def _get_data(
self, latitude: float, longitude: float, time: datetime
) -> Optional[Dict]:
# Expected dict:
# {
# "humidity": 0.69,
# "icon": "partly-cloudy-day",
# "temperature": 12.26,
# "wind": 3.49,
# "windBearing": 315
# }
#
# FitTrackee expects the following units:
# temperature: Celsius,
# humidity: in fraction (rather than percent)
# windSpeed: m/s
# windBearing: direction wind is from in degrees (0 is north)
#
# Expected icon values (for UI):
# - "clear-day",
# - "clear-night",
# - "cloudy",
# - "fog",
# - "partly-cloudy-day",
# - "partly-cloudy-night",
# - "rain",
# - "sleet",
# - "snow",
# - "wind"
pass
def get_weather(self, point: GPXTrackPoint) -> Optional[Dict]:
if not point.time:
# if there's no time associated with the point,
# we cannot get weather
return None
return self._get_data(point.latitude, point.longitude, point.time)

View File

@ -0,0 +1,37 @@
from datetime import datetime
from typing import Dict, Optional
import forecastio
import pytz
from .base_weather import BaseWeather
class DarkSky(BaseWeather):
# Deprecated (API will end on March 31st, 2023)
def _get_data(
self, latitude: float, longitude: float, time: datetime
) -> Optional[Dict]:
# get point time in UTC
point_time = (
pytz.utc.localize(time)
if time.tzinfo is None # naive datetime
else time
)
forecast = forecastio.load_forecast(
self.api_key,
latitude,
longitude,
time=point_time,
units='si',
)
weather = forecast.currently()
return {
'humidity': weather.humidity,
'icon': weather.icon,
'temperature': weather.temperature,
'wind': weather.windSpeed,
'windBearing': weather.windBearing,
}

View File

@ -0,0 +1,84 @@
from datetime import datetime, timedelta
from typing import Dict, Optional
import requests
from fittrackee import appLog
from .base_weather import BaseWeather
class VisualCrossing(BaseWeather):
def __init__(self, api_key: str):
super().__init__(api_key)
self.base_url = (
'https://weather.visualcrossing.com/'
'VisualCrossingWebServices/rest/services'
)
self.params = {
"key": self.api_key,
"iconSet": "icons1", # default value, same as Darksky
"unitGroup": "metric",
"contentType": "json",
"elements": (
"datetime,datetimeEpoch,temp,humidity,windspeed,"
"winddir,conditions,description,icon"
),
"include": "current", # to get only specific time data
}
@staticmethod
def _get_timestamp(time: datetime) -> int:
# The results are returned in the currentConditions field and are
# truncated to the hour requested (i.e. 2020-10-19T13:59:00 will return
# data at 2020-10-19T13:00:00).
# first, round datetime to nearest hour by truncating, and then adding
# an hour if the "real" time's number of minutes is 30 or more (we do
# this since the API only truncates)
trunc_time = time.replace(
second=0, microsecond=0, minute=0, hour=time.hour
) + timedelta(hours=time.minute // 30)
appLog.debug(
f'VC_weather: truncated time {time} ({time.timestamp()})'
f' to {trunc_time} ({trunc_time.timestamp()})'
)
return int(trunc_time.timestamp())
def _get_data(
self, latitude: float, longitude: float, time: datetime
) -> Optional[Dict]:
# All requests to the Timeline Weather API use the following the form:
# https://weather.visualcrossing.com/VisualCrossingWebServices/rest
# /services/timeline/[location]/[date1]/[date2]?key=YOUR_API_KEY
# location (required) is the address, partial address or
# latitude,longitude location for
# which to retrieve weather data. You can also use US ZIP Codes.
# date1 (optional) is the start date for which to retrieve weather
# data. All dates and times are in local time of the **location**
# specified.
url = (
f"{self.base_url}/timeline/{latitude},{longitude}"
f"/{self._get_timestamp(time)}"
)
appLog.debug(
f'VC_weather: getting weather from {url}'.replace(
self.api_key, '*****'
)
)
r = requests.get(url, params=self.params)
r.raise_for_status()
res = r.json()
weather = res['currentConditions']
data = {
'icon': weather['icon'],
'temperature': weather['temp'],
'humidity': weather['humidity'] / 100,
'wind': weather['windspeed'] * 1000 / (60 * 60), # km/h to m/s
'windBearing': weather['winddir'],
}
return data

View File

@ -0,0 +1,44 @@
import os
from typing import Dict, Optional, Union
from gpxpy.gpx import GPXTrackPoint
from fittrackee import appLog
from .dark_sky import DarkSky
from .visual_crossing import VisualCrossing
class WeatherService:
"""
Available API:
- DarkSky (deprecated, will end on March 31st, 2023)
- VisualCrossing
"""
def __init__(self) -> None:
self.weather_api = self._get_weather_api()
@staticmethod
def _get_weather_api() -> Union[DarkSky, VisualCrossing, None]:
weather_api_key: str = os.getenv('WEATHER_API_KEY', '')
weather_api_provider: str = os.getenv(
'WEATHER_API_PROVIDER', ''
).lower()
if not weather_api_key:
return None
if weather_api_provider == 'darksky': # deprecated
return DarkSky(weather_api_key)
if weather_api_provider == 'visualcrossing':
return VisualCrossing(weather_api_key)
return None
def get_weather(self, point: GPXTrackPoint) -> Optional[Dict]:
if not self.weather_api:
return None
try:
return self.weather_api.get_weather(point)
except Exception as e:
appLog.error(e)
return None