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 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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
|
@ -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
|
||||||
|
|
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