Add owm 3.0 API capabilities to get UVI reading into the fullscreen weather (again)
This commit is contained in:
		| @@ -17,6 +17,10 @@ from dateutil import tz | |||||||
|  |  | ||||||
| TEMP_UNITS = Literal["celsius", "fahrenheit"] | TEMP_UNITS = Literal["celsius", "fahrenheit"] | ||||||
| WIND_UNITS = Literal["meters_sec", "km_hour", "miles_hour", "knots", "beaufort"] | 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 = logging.getLogger(__name__) | ||||||
| logger.setLevel(level=logging.INFO) | logger.setLevel(level=logging.INFO) | ||||||
| @@ -27,42 +31,71 @@ def is_timestamp_within_range(timestamp: datetime, start_time: datetime, end_tim | |||||||
|     return start_time <= timestamp <= end_time |     return start_time <= timestamp <= end_time | ||||||
|  |  | ||||||
|  |  | ||||||
| class OpenWeatherMap: | def get_json_from_url(request_url): | ||||||
|     def __init__( |     response = requests.get(request_url) | ||||||
|         self, |  | ||||||
|         api_key: str, |  | ||||||
|         city_id: int, |  | ||||||
|         temp_unit: TEMP_UNITS = "celsius", |  | ||||||
|         wind_unit: WIND_UNITS = "meters_sec", |  | ||||||
|         language: str = "en", |  | ||||||
|         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.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}.") |  | ||||||
|  |  | ||||||
|     def get_current_weather(self) -> Dict: |  | ||||||
|         """ |  | ||||||
|         Gets current weather status from this API: https://openweathermap.org/current |  | ||||||
|         :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: |     if not response.ok: | ||||||
|         raise AssertionError( |         raise AssertionError( | ||||||
|             f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}" |             f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}" | ||||||
|         ) |         ) | ||||||
|         current_data = json.loads(response.text) |     return json.loads(response.text) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OpenWeatherMap: | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         api_key: str, | ||||||
|  |         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", | ||||||
|  |     ) -> None: | ||||||
|  |         self.api_key = api_key | ||||||
|  |         self.temp_unit = temp_unit | ||||||
|  |         self.wind_unit = wind_unit | ||||||
|  |         self.language = language | ||||||
|  |         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 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: | ||||||
|  |         """ | ||||||
|  |         Decodes the OWM current weather data for our purposes | ||||||
|  |         :return: | ||||||
|  |             Current weather as dictionary | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         current_data = self.get_weather_data_from_owm(weather="current") | ||||||
|  |  | ||||||
|         current_weather = {} |         current_weather = {} | ||||||
|         current_weather["detailed_status"] = current_data["weather"][0]["description"] |         current_weather["detailed_status"] = current_data["weather"][0]["description"] | ||||||
| @@ -80,10 +113,17 @@ class OpenWeatherMap: | |||||||
|         if "gust" in current_data["wind"]: |         if "gust" in current_data["wind"]: | ||||||
|             current_weather["wind_gust"] = self.get_converted_windspeed(current_data["wind"]["gust"]) |             current_weather["wind_gust"] = self.get_converted_windspeed(current_data["wind"]["gust"]) | ||||||
|         else: |         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["wind_gust"] = current_weather["wind"] | ||||||
|         current_weather["uvi"] = None  # TODO: this is no longer supported with 2.5 API, find alternative |         if "uvi" in current_data:  # this is only supported in v3.0 API | ||||||
|         current_weather["sunrise"] = datetime.fromtimestamp(current_data["sys"]["sunrise"], tz=self.tz_zone)  # unix timestamp -> to our timezone |             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) |         current_weather["sunset"] = datetime.fromtimestamp(current_data["sys"]["sunset"], tz=self.tz_zone) | ||||||
|  |  | ||||||
|         self.current_weather = current_weather |         self.current_weather = current_weather | ||||||
| @@ -92,21 +132,13 @@ class OpenWeatherMap: | |||||||
|  |  | ||||||
|     def get_weather_forecast(self) -> List[Dict]: |     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. |         What you get is a list of 40 forecasts for 3-hour time slices, totaling to 5 days. | ||||||
|         :return: |         :return: | ||||||
|             Forecasts data dictionary |             Forecasts data dictionary | ||||||
|         """ |         """ | ||||||
|         # |         # | ||||||
|         forecast_url = ( |         forecast_data = self.get_weather_data_from_owm(weather="forecast") | ||||||
|             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"] |  | ||||||
|  |  | ||||||
|         # Add forecast data to hourly_data_dict list of dictionaries |         # Add forecast data to hourly_data_dict list of dictionaries | ||||||
|         hourly_forecasts = [] |         hourly_forecasts = [] | ||||||
| @@ -134,10 +166,12 @@ class OpenWeatherMap: | |||||||
|                     "precip_probability": forecast["pop"] |                     "precip_probability": forecast["pop"] | ||||||
|                     * 100.0,  #  OWM value is unitless, directly converting to % scale |                     * 100.0,  #  OWM value is unitless, directly converting to % scale | ||||||
|                     "icon": forecast["weather"][0]["icon"], |                     "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 |         self.hourly_forecasts = hourly_forecasts | ||||||
|  |  | ||||||
| @@ -292,8 +326,9 @@ def main(): | |||||||
|  |  | ||||||
|     current_weather = owm.get_current_weather() |     current_weather = owm.get_current_weather() | ||||||
|     print(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)) |     print(owm.get_forecast_for_day(days_from_today=2)) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     main() |     main() | ||||||
|   | |||||||
| @@ -76,13 +76,15 @@ class Fullweather(inkycal_module): | |||||||
|         "api_key": { |         "api_key": { | ||||||
|             "label": "Please enter openweathermap api-key. You can create one for free on openweathermap", |             "label": "Please enter openweathermap api-key. You can create one for free on openweathermap", | ||||||
|         }, |         }, | ||||||
|         "location": { |         "latitude": {"label": "Please enter your location' geographical latitude. E.g. 51.51 for London."}, | ||||||
|             "label": "Please enter your location ID found in the url " |         "longitude": {"label": "Please enter your location' geographical longitude. E.g. -0.13 for London."}, | ||||||
|             + "e.g. https://openweathermap.org/city/4893171 -> ID is 4893171" |  | ||||||
|         }, |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     optional = { |     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"]}, |         "orientation": {"label": "Please select the desired orientation", "options": ["vertical", "horizontal"]}, | ||||||
|         "temp_unit": { |         "temp_unit": { | ||||||
|             "label": "Which temperature unit should be used?", |             "label": "Which temperature unit should be used?", | ||||||
| @@ -142,10 +144,15 @@ class Fullweather(inkycal_module): | |||||||
|  |  | ||||||
|         # required parameters |         # required parameters | ||||||
|         self.api_key = config["api_key"] |         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"]) |         self.font_size = int(config["fontsize"]) | ||||||
|  |  | ||||||
|         # optional parameters |         # 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: |         if "orientation" in config: | ||||||
|             self.orientation = config["orientation"] |             self.orientation = config["orientation"] | ||||||
|             assert self.orientation in ["horizontal", "vertical"] |             assert self.orientation in ["horizontal", "vertical"] | ||||||
| @@ -310,7 +317,8 @@ class Fullweather(inkycal_module): | |||||||
|             self.image.paste(uvIcon, (15, ux_y)) |             self.image.paste(uvIcon, (15, ux_y)) | ||||||
|  |  | ||||||
|             # uvindex |             # 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) |             uvFont = self.get_font("Bold", self.font_size + 8) | ||||||
|             image_draw.text((65, ux_y), uvString, font=uvFont, fill=(255, 255, 255)) |             image_draw.text((65, ux_y), uvString, font=uvFont, fill=(255, 255, 255)) | ||||||
|  |  | ||||||
| @@ -602,7 +610,9 @@ class Fullweather(inkycal_module): | |||||||
|         # Get the weather |         # Get the weather | ||||||
|         self.my_owm = OpenWeatherMap( |         self.my_owm = OpenWeatherMap( | ||||||
|             api_key=self.api_key, |             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, |             temp_unit=self.temp_unit, | ||||||
|             wind_unit=self.wind_unit, |             wind_unit=self.wind_unit, | ||||||
|             language=self.language, |             language=self.language, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 mrbwburns
					mrbwburns