From d4f9a7a8459c210f6953f7dc470e56da6dfc00ff Mon Sep 17 00:00:00 2001 From: mrbwburns <> Date: Sun, 4 Feb 2024 10:01:49 -0800 Subject: [PATCH] Add owm 3.0 API capabilities to get UVI reading into the fullscreen weather (again) --- inkycal/custom/openweathermap_wrapper.py | 105 +++++++++++++++-------- inkycal/modules/inkycal_fullweather.py | 24 ++++-- 2 files changed, 87 insertions(+), 42 deletions(-) diff --git a/inkycal/custom/openweathermap_wrapper.py b/inkycal/custom/openweathermap_wrapper.py index f1c69fd..6cd4405 100644 --- a/inkycal/custom/openweathermap_wrapper.py +++ b/inkycal/custom/openweathermap_wrapper.py @@ -17,6 +17,10 @@ from dateutil import tz TEMP_UNITS = Literal["celsius", "fahrenheit"] WIND_UNITS = Literal["meters_sec", "km_hour", "miles_hour", "knots", "beaufort"] +WEATHER_TYPE = Literal["current", "forecast"] +API_VERSIONS = Literal["2.5", "3.0"] + +API_BASE_URL = "https://api.openweathermap.org/data" logger = logging.getLogger(__name__) logger.setLevel(level=logging.INFO) @@ -27,43 +31,72 @@ def is_timestamp_within_range(timestamp: datetime, start_time: datetime, end_tim return start_time <= timestamp <= end_time +def get_json_from_url(request_url): + response = requests.get(request_url) + if not response.ok: + raise AssertionError( + f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}" + ) + return json.loads(response.text) + + class OpenWeatherMap: def __init__( self, api_key: str, - city_id: int, + city_id: int = None, + lat: float = None, + lon: float = None, + api_version: API_VERSIONS = "2.5", temp_unit: TEMP_UNITS = "celsius", wind_unit: WIND_UNITS = "meters_sec", language: str = "en", - tz_name: str = "UTC" + tz_name: str = "UTC", ) -> None: self.api_key = api_key - self.city_id = city_id self.temp_unit = temp_unit self.wind_unit = wind_unit self.language = language - self._api_version = "2.5" - self._base_url = f"https://api.openweathermap.org/data/{self._api_version}" + self._api_version = api_version + if self._api_version == "3.0": + assert type(lat) is float and type(lon) is float + self.location_substring = ( + f"lat={str(lat)}&lon={str(lon)}" if (lat is not None and lon is not None) else f"id={str(city_id)}" + ) + self.tz_zone = tz.gettz(tz_name) - logger.info(f"OWM wrapper initialized for city id {self.city_id}, language {self.language} and timezone {tz_name}.") + logger.info( + f"OWM wrapper initialized for API version {self._api_version}, language {self.language} and timezone {tz_name}." + ) + + def get_weather_data_from_owm(self, weather: WEATHER_TYPE): + # Gets current weather or forecast from the configured OWM API. + + if weather == "current": + # Gets current weather status from the 2.5 API: https://openweathermap.org/current + # This is primarily using the 2.5 API since the 3.0 API actually has less info + weather_url = f"{API_BASE_URL}/2.5/weather?{self.location_substring}&appid={self.api_key}&units=Metric&lang={self.language}" + weather_data = get_json_from_url(weather_url) + # Only if we do have a 3.0 API-enabled key, we can also get the UVI reading from that endpoint: https://openweathermap.org/api/one-call-3 + if self._api_version == "3.0": + weather_url = f"{API_BASE_URL}/3.0/onecall?{self.location_substring}&appid={self.api_key}&exclude=minutely,hourly,daily&units=Metric&lang={self.language}" + weather_data["uvi"] = get_json_from_url(weather_url)["current"]["uvi"] + elif weather == "forecast": + # Gets weather forecasts from the 2.5 API: https://openweathermap.org/forecast5 + # This is only using the 2.5 API since the 3.0 API actually has less info + weather_url = f"{API_BASE_URL}/2.5/forecast?{self.location_substring}&appid={self.api_key}&units=Metric&lang={self.language}" + weather_data = get_json_from_url(weather_url)["list"] + return weather_data def get_current_weather(self) -> Dict: """ - Gets current weather status from this API: https://openweathermap.org/current + Decodes the OWM current weather data for our purposes :return: Current weather as dictionary """ - # Gets weather forecast from this API: - current_weather_url = ( - f"{self._base_url}/weather?id={self.city_id}&appid={self.api_key}&units=Metric&lang={self.language}" - ) - response = requests.get(current_weather_url) - if not response.ok: - raise AssertionError( - f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}" - ) - current_data = json.loads(response.text) - + + current_data = self.get_weather_data_from_owm(weather="current") + current_weather = {} current_weather["detailed_status"] = current_data["weather"][0]["description"] current_weather["weather_icon_name"] = current_data["weather"][0]["icon"] @@ -80,10 +113,17 @@ class OpenWeatherMap: if "gust" in current_data["wind"]: current_weather["wind_gust"] = self.get_converted_windspeed(current_data["wind"]["gust"]) else: - logger.info(f"OpenWeatherMap response did not contain a wind gust speed. Using base wind: {current_weather['wind']} m/s.") + logger.info( + f"OpenWeatherMap response did not contain a wind gust speed. Using base wind: {current_weather['wind']} m/s." + ) current_weather["wind_gust"] = current_weather["wind"] - current_weather["uvi"] = None # TODO: this is no longer supported with 2.5 API, find alternative - current_weather["sunrise"] = datetime.fromtimestamp(current_data["sys"]["sunrise"], tz=self.tz_zone) # unix timestamp -> to our timezone + if "uvi" in current_data: # this is only supported in v3.0 API + current_weather["uvi"] = current_data["uvi"] + else: + current_weather["uvi"] = None + current_weather["sunrise"] = datetime.fromtimestamp( + current_data["sys"]["sunrise"], tz=self.tz_zone + ) # unix timestamp -> to our timezone current_weather["sunset"] = datetime.fromtimestamp(current_data["sys"]["sunset"], tz=self.tz_zone) self.current_weather = current_weather @@ -92,21 +132,13 @@ class OpenWeatherMap: def get_weather_forecast(self) -> List[Dict]: """ - Gets weather forecasts from this API: https://openweathermap.org/forecast5 + Decodes the OWM weather forecast for our purposes What you get is a list of 40 forecasts for 3-hour time slices, totaling to 5 days. :return: Forecasts data dictionary """ # - forecast_url = ( - f"{self._base_url}/forecast?id={self.city_id}&appid={self.api_key}&units=Metric&lang={self.language}" - ) - response = requests.get(forecast_url) - if not response.ok: - raise AssertionError( - f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}" - ) - forecast_data = json.loads(response.text)["list"] + forecast_data = self.get_weather_data_from_owm(weather="forecast") # Add forecast data to hourly_data_dict list of dictionaries hourly_forecasts = [] @@ -134,10 +166,12 @@ class OpenWeatherMap: "precip_probability": forecast["pop"] * 100.0, # OWM value is unitless, directly converting to % scale "icon": forecast["weather"][0]["icon"], - "datetime": datetime.fromtimestamp(forecast["dt"], tz=self.tz_zone) + "datetime": datetime.fromtimestamp(forecast["dt"], tz=self.tz_zone), } ) - logger.debug(f"Added rain forecast at {datetime.fromtimestamp(forecast['dt'], tz=self.tz_zone)}: {precip_mm}") + logger.debug( + f"Added rain forecast at {datetime.fromtimestamp(forecast['dt'], tz=self.tz_zone)}: {precip_mm}" + ) self.hourly_forecasts = hourly_forecasts @@ -155,7 +189,7 @@ class OpenWeatherMap: """ # Make sure hourly forecasts are up to date _ = self.get_weather_forecast() - + # Calculate the start and end times for the specified number of days from now current_time = datetime.now(tz=self.tz_zone) start_time = ( @@ -292,8 +326,9 @@ def main(): current_weather = owm.get_current_weather() print(current_weather) - hourly_forecasts = owm.get_weather_forecast() + _ = owm.get_weather_forecast() print(owm.get_forecast_for_day(days_from_today=2)) + if __name__ == "__main__": main() diff --git a/inkycal/modules/inkycal_fullweather.py b/inkycal/modules/inkycal_fullweather.py index d0ef28e..55ce044 100644 --- a/inkycal/modules/inkycal_fullweather.py +++ b/inkycal/modules/inkycal_fullweather.py @@ -76,13 +76,15 @@ class Fullweather(inkycal_module): "api_key": { "label": "Please enter openweathermap api-key. You can create one for free on openweathermap", }, - "location": { - "label": "Please enter your location ID found in the url " - + "e.g. https://openweathermap.org/city/4893171 -> ID is 4893171" - }, + "latitude": {"label": "Please enter your location' geographical latitude. E.g. 51.51 for London."}, + "longitude": {"label": "Please enter your location' geographical longitude. E.g. -0.13 for London."}, } optional = { + "api_version": { + "label": "Please enter openweathermap api version. Default is '2.5'.", + "options": ["2.5", "3.0"], + }, "orientation": {"label": "Please select the desired orientation", "options": ["vertical", "horizontal"]}, "temp_unit": { "label": "Which temperature unit should be used?", @@ -142,10 +144,15 @@ class Fullweather(inkycal_module): # required parameters self.api_key = config["api_key"] - self.location = int(config["location"]) + self.location_lat = float(config["latitude"]) + self.location_lon = float(config["longitude"]) self.font_size = int(config["fontsize"]) # optional parameters + if "api_version" in config and config["api_version"] == "3.0": + self.owm_api_version = "3.0" + else: + self.owm_api_version = "2.5" if "orientation" in config: self.orientation = config["orientation"] assert self.orientation in ["horizontal", "vertical"] @@ -310,7 +317,8 @@ class Fullweather(inkycal_module): self.image.paste(uvIcon, (15, ux_y)) # uvindex - uvString = f"{self.current_weather['uvi'] if self.current_weather['uvi'] else '0'}" + uvi = self.current_weather["uvi"] if self.current_weather["uvi"] else 0.0 + uvString = f"{uvi:.1f}" uvFont = self.get_font("Bold", self.font_size + 8) image_draw.text((65, ux_y), uvString, font=uvFont, fill=(255, 255, 255)) @@ -602,7 +610,9 @@ class Fullweather(inkycal_module): # Get the weather self.my_owm = OpenWeatherMap( api_key=self.api_key, - city_id=self.location, + api_version=self.owm_api_version, + lat=self.location_lat, + lon=self.location_lon, temp_unit=self.temp_unit, wind_unit=self.wind_unit, language=self.language,