[#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 typing import Optional
from platypush.message.event import Event from platypush.message.event import Event
@ -8,17 +9,90 @@ class NewWeatherConditionEvent(Event):
Event triggered when the weather condition changes Event triggered when the weather condition changes
""" """
def __init__(self, *args, plugin_name: Optional[str] = None, **kwargs): def __init__(
super().__init__(*args, plugin_name=plugin_name, **kwargs) 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): class NewPrecipitationForecastEvent(Event):
""" """
Event triggered when the precipitation forecast changes Event triggered when the precipitation forecast changes
""" """
def __init__(self, *args, plugin_name: Optional[str] = None, average: float, total: float,
time_frame: int, **kwargs): def __init__(
super().__init__(*args, plugin_name=plugin_name, average=average, total=total, time_frame=time_frame, **kwargs) 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: # vim:sw=4:ts=4:et:

View File

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

View File

@ -1,14 +1,41 @@
from abc import ABC, abstractmethod 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. 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 @action
@abstractmethod @abstractmethod
def get_current_weather(self, *args, **kwargs): 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 from typing import Optional
import requests
from platypush.plugins import action from platypush.plugins import action
from platypush.plugins.http.request import HttpRequestPlugin
from platypush.plugins.weather import WeatherPlugin 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 OpenWeatherMap plugin.
shut down their API.
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' base_url = 'https://api.openweathermap.org/data/2.5/weather'
def __init__(self, token: str, location: Optional[str] = None, city_id: Optional[int] = None, def __init__(
lat: Optional[float] = None, long: Optional[float] = None, self,
zip_code: Optional[str] = None, units: str = 'metric', **kwargs): 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 token: OpenWeatherMap API token.
:param location: If set, then this location will be used by default for weather lookup. If multiple locations :param location: If set, then this location will be used by default for
share the same name you can disambiguate by specifying the country code as well - e.g. ``London,GB``. weather lookup. If multiple locations share the same name you can
:param city_id: If set, then this city ID will be used by default for weather lookup. The full list of city IDs disambiguate by specifying the country code as well - e.g.
is available `here <https://bulk.openweathermap.org/sample/>`_. ``London,GB``.
:param lat: If lat/long are set, then the weather by default will be retrieved for the specified geo location. :param city_id: If set, then this city ID will be used by default for
:param long: If lat/long are set, then the weather by default will be retrieved for the specified geo location. weather lookup. The full list of city IDs is available `here
:param zip_code: If set, then this ZIP code (should be in the form ``zip,country_code``) will be used by default <https://bulk.openweathermap.org/sample/>`_.
for weather lookup. :param lat: If lat/long are set, then the weather by default will be
:param units: Supported: ``metric`` (default), ``standard`` and ``imperial``. 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._token = token
self._location_query = None self._location_query = None
self._location_query = self._get_location_query(location=location, city_id=city_id, lat=lat, long=long, self._location_query = self._get_location_query(
zip_code=zip_code) location=location, city_id=city_id, lat=lat, long=long, zip_code=zip_code
)
self.units = units self.units = units
def _get_location_query(self, location: Optional[str] = None, city_id: Optional[int] = None, def _get_location_query(
lat: Optional[float] = None, long: Optional[float] = None, self,
zip_code: Optional[str] = None) -> dict: 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: if city_id:
return {'id': city_id} return {'id': city_id}
if lat and long: if lat and long:
@ -52,59 +77,17 @@ class WeatherOpenweathermapPlugin(HttpRequestPlugin, WeatherPlugin):
return self._location_query return self._location_query
@action @action
def get(self, url, **kwargs): def get_current_weather(
kwargs['params'] = { self,
'appid': self._token, *_,
**kwargs.get('params', {}), location: Optional[str] = None,
} city_id: Optional[int] = None,
lat: Optional[float] = None,
return super().get(url, **kwargs) long: Optional[float] = None,
zip_code: Optional[str] = None,
@staticmethod units: Optional[str] = None,
def _convert_percentage(perc: Optional[float]) -> Optional[float]: **__
if perc is None: ) -> dict:
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:
""" """
Returns the current weather. Returns the current weather.
@ -114,11 +97,20 @@ class WeatherOpenweathermapPlugin(HttpRequestPlugin, WeatherPlugin):
:param long: Override the ``long`` configuration value. :param long: Override the ``long`` configuration value.
:param zip_code: Override the ``zip_code`` configuration value. :param zip_code: Override the ``zip_code`` configuration value.
:param units: Override the ``units`` configuration value. :param units: Override the ``units`` configuration value.
:return: .. schema:: weather.openweathermap.WeatherSchema
""" """
params = { params = {
'appid': self._token,
'units': units or self.units, '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 rs = requests.get(self.base_url, params=params, timeout=10)
return self._convert_weather_response(response) rs.raise_for_status()
return dict(WeatherSchema().dump(rs.json()))

View File

@ -1,5 +1,6 @@
manifest: manifest:
events: {} events:
- platypush.message.event.weather.NewWeatherConditionEvent
install: install:
pip: [] pip: []
package: platypush.plugins.weather.openweathermap 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