[#308] Refactored weather.openweathermap plugin.

This commit is contained in:
Fabio Manganiello 2023-11-19 00:10:10 +01:00
parent 6108cbb621
commit b800899859
Signed by: blacklight
GPG key ID: D90FBA7F76362774
7 changed files with 333 additions and 90 deletions

View file

@ -1,3 +1,4 @@
from datetime import datetime
from typing import Optional
from platypush.message.event import Event
@ -8,17 +9,90 @@ class NewWeatherConditionEvent(Event):
Event triggered when the weather condition changes
"""
def __init__(self, *args, plugin_name: Optional[str] = None, **kwargs):
super().__init__(*args, plugin_name=plugin_name, **kwargs)
def __init__(
self,
*args,
plugin_name: str,
summary: Optional[str] = None,
icon: Optional[str] = None,
precip_intensity: Optional[float] = None,
precip_type: Optional[str] = None,
temperature: Optional[float] = None,
apparent_temperature: Optional[float] = None,
humidity: Optional[float] = None,
pressure: Optional[float] = None,
wind_speed: Optional[float] = None,
wind_gust: Optional[float] = None,
wind_direction: Optional[float] = None,
cloud_cover: Optional[float] = None,
visibility: Optional[float] = None,
sunrise: Optional[datetime] = None,
sunset: Optional[datetime] = None,
**kwargs,
):
"""
:param plugin_name: Plugin that triggered the event.
:param summary: Summary of the weather condition.
:param icon: Icon representing the weather condition.
:param precip_intensity: Intensity of the precipitation.
:param precip_type: Type of precipitation.
:param temperature: Temperature, in the configured unit system.
:param apparent_temperature: Apparent temperature, in the configured
unit system.
:param humidity: Humidity percentage, between 0 and 100.
:param pressure: Pressure, in the configured unit system.
:param wind_speed: Wind speed, in the configured unit system.
:param wind_gust: Wind gust, in the configured unit system.
:param wind_direction: Wind direction, in degrees.
:param cloud_cover: Cloud cover percentage, between 0 and 100.
:param visibility: Visibility, in meters.
:param sunrise: Sunrise time.
:param sunset: Sunset time.
"""
super().__init__(
*args,
plugin_name=plugin_name,
summary=summary,
icon=icon,
precip_intensity=precip_intensity,
precip_type=precip_type,
temperature=temperature,
apparent_temperature=apparent_temperature,
humidity=humidity,
pressure=pressure,
wind_speed=wind_speed,
wind_gust=wind_gust,
wind_direction=wind_direction,
cloud_cover=cloud_cover,
visibility=visibility,
sunrise=sunrise,
sunset=sunset,
**kwargs,
)
class NewPrecipitationForecastEvent(Event):
"""
Event triggered when the precipitation forecast changes
"""
def __init__(self, *args, plugin_name: Optional[str] = None, average: float, total: float,
time_frame: int, **kwargs):
super().__init__(*args, plugin_name=plugin_name, average=average, total=total, time_frame=time_frame, **kwargs)
def __init__(
self,
*args,
plugin_name: Optional[str] = None,
average: float,
total: float,
time_frame: int,
**kwargs,
):
super().__init__(
*args,
plugin_name=plugin_name,
average=average,
total=total,
time_frame=time_frame,
**kwargs,
)
# vim:sw=4:ts=4:et:

View file

@ -23,7 +23,7 @@ class TodoistPlugin(Plugin):
Todoist integration.
You'll also need a Todoist token. You can get it `here
<https://todoist.com/prefs/integrations>`_.
<https://todoist.com/prefs/integrations>`_.
"""
_sync_timeout = 60.0
@ -31,7 +31,7 @@ class TodoistPlugin(Plugin):
def __init__(self, api_token: str, **kwargs):
"""
:param api_token: Todoist API token. You can get it `here
<https://todoist.com/prefs/integrations>`_.
<https://todoist.com/prefs/integrations>`_.
"""
super().__init__(**kwargs)

View file

@ -1,14 +1,41 @@
from abc import ABC, abstractmethod
from typing import Optional
from platypush.plugins import Plugin, action
from platypush.message.event.weather import NewWeatherConditionEvent
from platypush.plugins import RunnablePlugin, action
from platypush.utils import get_plugin_name_by_class
class WeatherPlugin(Plugin, ABC):
class WeatherPlugin(RunnablePlugin, ABC):
"""
Base class for weather plugins.
"""
def __init__(self, poll_interval: Optional[float] = 120, **kwargs):
super().__init__(poll_interval=poll_interval, **kwargs)
self._latest_weather = None
@action
@abstractmethod
def get_current_weather(self, *args, **kwargs):
raise NotImplementedError
raise NotImplementedError("get_current_weather not implemented")
def main(self):
while not self.should_stop():
try:
current_weather = dict(self.get_current_weather().output or {}) # type: ignore
current_weather.pop("time", None)
if current_weather != self._latest_weather:
self._bus.post(
NewWeatherConditionEvent(
plugin_name=get_plugin_name_by_class(self.__class__),
**current_weather
)
)
self._latest_weather = current_weather
except Exception as e:
self.logger.exception(e)
finally:
self.wait_stop(self.poll_interval)

View file

@ -1,44 +1,69 @@
from typing import Optional
import requests
from platypush.plugins import action
from platypush.plugins.http.request import HttpRequestPlugin
from platypush.plugins.weather import WeatherPlugin
from platypush.schemas.weather.openweathermap import WeatherSchema
class WeatherOpenweathermapPlugin(HttpRequestPlugin, WeatherPlugin):
class WeatherOpenweathermapPlugin(WeatherPlugin):
"""
OpenWeatherMap plugin. This is the advised plugin to use for weather forecasts since Darksky has officially
shut down their API.
OpenWeatherMap plugin.
You'll need an API token from `OpenWeatherMap <https://openweathermap.org/api>`_ in order to use this API.
You'll need an API token from `OpenWeatherMap
<https://openweathermap.org/api>`_ in order to use this API.
"""
base_url = 'https://api.openweathermap.org/data/2.5/weather'
def __init__(self, token: str, location: Optional[str] = None, city_id: Optional[int] = None,
lat: Optional[float] = None, long: Optional[float] = None,
zip_code: Optional[str] = None, units: str = 'metric', **kwargs):
def __init__(
self,
token: str,
location: Optional[str] = None,
city_id: Optional[int] = None,
lat: Optional[float] = None,
long: Optional[float] = None,
zip_code: Optional[str] = None,
units: str = 'metric',
**kwargs
):
"""
:param token: OpenWeatherMap API token.
:param location: If set, then this location will be used by default for weather lookup. If multiple locations
share the same name you can disambiguate by specifying the country code as well - e.g. ``London,GB``.
:param city_id: If set, then this city ID will be used by default for weather lookup. The full list of city IDs
is available `here <https://bulk.openweathermap.org/sample/>`_.
:param lat: If lat/long are set, then the weather by default will be retrieved for the specified geo location.
:param long: If lat/long are set, then the weather by default will be retrieved for the specified geo location.
:param zip_code: If set, then this ZIP code (should be in the form ``zip,country_code``) will be used by default
for weather lookup.
:param units: Supported: ``metric`` (default), ``standard`` and ``imperial``.
:param location: If set, then this location will be used by default for
weather lookup. If multiple locations share the same name you can
disambiguate by specifying the country code as well - e.g.
``London,GB``.
:param city_id: If set, then this city ID will be used by default for
weather lookup. The full list of city IDs is available `here
<https://bulk.openweathermap.org/sample/>`_.
:param lat: If lat/long are set, then the weather by default will be
retrieved for the specified geo location.
:param long: If lat/long are set, then the weather by default will be
retrieved for the specified geo location.
:param zip_code: If set, then this ZIP code (should be in the form
``zip,country_code``) will be used by default for weather lookup.
:param units: Supported: ``metric`` (default), ``standard`` and
``imperial``.
:param poll_interval: How often the weather should be refreshed, in
seconds.
"""
super().__init__(method='get', output='json', **kwargs)
super().__init__(**kwargs)
self._token = token
self._location_query = None
self._location_query = self._get_location_query(location=location, city_id=city_id, lat=lat, long=long,
zip_code=zip_code)
self._location_query = self._get_location_query(
location=location, city_id=city_id, lat=lat, long=long, zip_code=zip_code
)
self.units = units
def _get_location_query(self, location: Optional[str] = None, city_id: Optional[int] = None,
lat: Optional[float] = None, long: Optional[float] = None,
zip_code: Optional[str] = None) -> dict:
def _get_location_query(
self,
location: Optional[str] = None,
city_id: Optional[int] = None,
lat: Optional[float] = None,
long: Optional[float] = None,
zip_code: Optional[str] = None,
) -> dict:
if city_id:
return {'id': city_id}
if lat and long:
@ -52,59 +77,17 @@ class WeatherOpenweathermapPlugin(HttpRequestPlugin, WeatherPlugin):
return self._location_query
@action
def get(self, url, **kwargs):
kwargs['params'] = {
'appid': self._token,
**kwargs.get('params', {}),
}
return super().get(url, **kwargs)
@staticmethod
def _convert_percentage(perc: Optional[float]) -> Optional[float]:
if perc is None:
return
return perc / 100.
@staticmethod
def _m_to_km(m: Optional[float]) -> Optional[float]:
if m is None:
return
return m / 1000.
@staticmethod
def _get_precip_type(response: dict) -> Optional[str]:
if response.get('snow', {}).get('1h', 0) > 0:
return 'snow'
if response.get('rain', {}).get('1h', 0) > 0:
return 'rain'
return
@classmethod
def _convert_weather_response(cls, response: dict) -> dict:
return {
'time': response.get('dt'),
'summary': response.get('weather', [{'main': 'Unknown'}])[0].get('main', 'Unknown'),
'icon': response.get('weather', [{'icon': 'unknown'}])[0].get('icon', 'unknown'),
'precipIntensity': response.get('rain', response.get('snow', {})).get('1h', 0),
'precipType': cls._get_precip_type(response),
'temperature': response.get('main', {}).get('temp'),
'apparentTemperature': response.get('main', {}).get('feels_like'),
'humidity': cls._convert_percentage(response.get('main', {}).get('humidity')),
'pressure': response.get('main', {}).get('pressure'),
'windSpeed': response.get('wind', {}).get('speed'),
'windDirection': response.get('wind', {}).get('deg'),
'windGust': response.get('wind', {}).get('gust'),
'cloudCover': cls._convert_percentage(response.get('clouds', {}).get('all')),
'visibility': cls._m_to_km(response.get('visibility')),
'sunrise': response.get('sys', {}).get('sunrise'),
'sunset': response.get('sys', {}).get('sunset'),
}
@action
def get_current_weather(self, *, location: Optional[str] = None, city_id: Optional[int] = None,
lat: Optional[float] = None, long: Optional[float] = None, zip_code: Optional[str] = None,
units: Optional[str] = None, **kwargs) -> dict:
def get_current_weather(
self,
*_,
location: Optional[str] = None,
city_id: Optional[int] = None,
lat: Optional[float] = None,
long: Optional[float] = None,
zip_code: Optional[str] = None,
units: Optional[str] = None,
**__
) -> dict:
"""
Returns the current weather.
@ -114,11 +97,20 @@ class WeatherOpenweathermapPlugin(HttpRequestPlugin, WeatherPlugin):
:param long: Override the ``long`` configuration value.
:param zip_code: Override the ``zip_code`` configuration value.
:param units: Override the ``units`` configuration value.
:return: .. schema:: weather.openweathermap.WeatherSchema
"""
params = {
'appid': self._token,
'units': units or self.units,
**self._get_location_query(location=location, city_id=city_id, lat=lat, long=long)
**self._get_location_query(
location=location,
city_id=city_id,
lat=lat,
long=long,
zip_code=zip_code,
),
}
response = self.get(self.base_url, params=params).output
return self._convert_weather_response(response)
rs = requests.get(self.base_url, params=params, timeout=10)
rs.raise_for_status()
return dict(WeatherSchema().dump(rs.json()))

View file

@ -1,5 +1,6 @@
manifest:
events: {}
events:
- platypush.message.event.weather.NewWeatherConditionEvent
install:
pip: []
package: platypush.plugins.weather.openweathermap

View file

View file

@ -0,0 +1,149 @@
from typing import Optional
from marshmallow import fields, pre_dump
from marshmallow.schema import Schema
from platypush.schemas import DateTime
def _get_precip_type(response: dict) -> Optional[str]:
if response.get('snow', {}).get('1h', 0) > 0:
return 'snow'
if response.get('rain', {}).get('1h', 0) > 0:
return 'rain'
return None
class WeatherSchema(Schema):
"""
Schema for weather data.
"""
summary = fields.Function(
lambda obj: obj.get('weather', [{'main': 'Unknown'}])[0].get('main', 'Unknown'),
metadata={
'description': 'Summary of the weather condition',
'example': 'Cloudy',
},
)
icon = fields.Function(
lambda obj: obj.get('weather', [{'icon': 'unknown'}])[0].get('icon', 'unknown'),
metadata={
'description': 'Icon representing the weather condition',
'example': 'cloudy',
},
)
precip_intensity = fields.Function(
lambda obj: obj.get('rain', obj.get('snow', {})).get('1h', 0),
metadata={
'description': 'Intensity of the precipitation',
'example': 0.0,
},
)
precip_type = fields.Function(
_get_precip_type,
metadata={
'description': 'Type of precipitation',
'example': 'rain',
},
)
temperature = fields.Function(
lambda obj: obj.get('main', {}).get('temp'),
metadata={
'description': 'Temperature in the configured unit of measure',
'example': 10.0,
},
)
apparent_temperature = fields.Function(
lambda obj: obj.get('main', {}).get('feels_like'),
metadata={
'description': 'Apparent temperature in the configured unit of measure',
'example': 9.0,
},
)
humidity = fields.Function(
lambda obj: obj.get('main', {}).get('humidity'),
metadata={
'description': 'Humidity percentage, between 0 and 100',
'example': 30,
},
)
pressure = fields.Function(
lambda obj: obj.get('main', {}).get('pressure'),
metadata={
'description': 'Pressure in hPa',
'example': 1000.0,
},
)
wind_speed = fields.Function(
lambda obj: obj.get('wind', {}).get('speed'),
metadata={
'description': 'Wind speed in the configured unit of measure',
'example': 10.0,
},
)
wind_direction = fields.Function(
lambda obj: obj.get('wind', {}).get('deg'),
metadata={
'description': 'Wind direction in degrees',
'example': 180.0,
},
)
wind_gust = fields.Function(
lambda obj: obj.get('wind', {}).get('gust'),
metadata={
'description': 'Wind gust in the configured unit of measure',
'example': 15.0,
},
)
cloud_cover = fields.Function(
lambda obj: obj.get('clouds', {}).get('all'),
metadata={
'description': 'Cloud cover percentage between 0 and 100',
'example': 0.5,
},
)
visibility = fields.Float(
metadata={
'description': 'Visibility in meters',
'example': 2000.0,
},
)
sunrise = DateTime(
metadata={
'description': 'Sunrise time',
'example': '2020-01-01T06:00:00+00:00',
},
)
sunset = DateTime(
metadata={
'description': 'Sunset time',
'example': '2020-01-01T18:00:00+00:00',
},
)
@pre_dump
def _pre_dump(self, data: dict, **_) -> dict:
sun_data = data.pop('sys', {})
sunrise = sun_data.get('sunrise')
sunset = sun_data.get('sunset')
if sunrise is not None:
data['sunrise'] = sunrise
if sunset is not None:
data['sunset'] = sunset
return data