forked from platypush/platypush
[#308] Refactored weather.openweathermap
plugin.
This commit is contained in:
parent
6108cbb621
commit
b800899859
7 changed files with 333 additions and 90 deletions
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
manifest:
|
||||
events: {}
|
||||
events:
|
||||
- platypush.message.event.weather.NewWeatherConditionEvent
|
||||
install:
|
||||
pip: []
|
||||
package: platypush.plugins.weather.openweathermap
|
||||
|
|
0
platypush/schemas/weather/__init__.py
Normal file
0
platypush/schemas/weather/__init__.py
Normal file
149
platypush/schemas/weather/openweathermap.py
Normal file
149
platypush/schemas/weather/openweathermap.py
Normal 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
|
Loading…
Reference in a new issue