-
-
-
-
@@ -184,11 +172,20 @@
+
+
diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/WeatherIcon.vue b/platypush/backend/http/webapp/src/components/panels/Entities/WeatherIcon.vue
new file mode 100644
index 0000000000..0c0e2ead3e
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/WeatherIcon.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
index b4d89cd52b..0c986e2865 100644
--- a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
@@ -23,6 +23,14 @@
}
},
+ "weather_forecast": {
+ "name": "Weather Forecast",
+ "name_plural": "Weather Forecast",
+ "icon": {
+ "class": "fas fa-cloud-sun-rain"
+ }
+ },
+
"button": {
"name": "Button",
"name_plural": "Buttons",
diff --git a/platypush/entities/managers/weather.py b/platypush/entities/managers/weather.py
index 3bab2f4765..39300f49e2 100644
--- a/platypush/entities/managers/weather.py
+++ b/platypush/entities/managers/weather.py
@@ -11,40 +11,41 @@ class WeatherEntityManager(EntityManager, ABC):
Base class for integrations that support weather reports.
"""
- def transform_entities(self, entities: List[dict]):
- from platypush.entities.weather import Weather
+ def transform_entities(self, entities: List[dict], *, type: str):
+ from platypush.entities.weather import Weather, WeatherForecast
if not entities:
return []
- weather = entities[0]
plugin = get_plugin_name_by_class(self.__class__)
- return super().transform_entities(
- [
- Weather(
- id=plugin,
- name='Weather',
- summary=weather.get('summary'),
- icon=weather.get('icon'),
- image=weather.get('image'),
- precip_intensity=weather.get('precip_intensity'),
- precip_type=weather.get('precip_type'),
- temperature=weather.get('temperature'),
- apparent_temperature=weather.get('apparent_temperature'),
- humidity=weather.get('humidity'),
- pressure=weather.get('pressure'),
- rain_chance=weather.get('rain_chance'),
- wind_speed=weather.get('wind_speed'),
- wind_direction=weather.get('wind_direction'),
- wind_gust=weather.get('wind_gust'),
- cloud_cover=weather.get('cloud_cover'),
- visibility=weather.get('visibility'),
- sunrise=weather.get('sunrise'),
- sunset=weather.get('sunset'),
- units=weather.get('units'),
- )
- ]
- )
+
+ if type == 'weather':
+ # Current weather response
+ weather = entities[0]
+ weather.pop('time', None)
+ return super().transform_entities(
+ [
+ Weather(
+ id=f'{plugin}:weather',
+ name='Weather',
+ **weather,
+ )
+ ]
+ )
+
+ # Weather forecast response
+ if type == 'forecast':
+ return super().transform_entities(
+ [
+ WeatherForecast(
+ id=f'{plugin}:forecast',
+ name='Forecast',
+ forecast=entities,
+ )
+ ]
+ )
+
+ raise AssertionError(f'Unexpected weather entity type: {type}')
@abstractmethod
def status(self, *_, **__):
diff --git a/platypush/entities/weather.py b/platypush/entities/weather.py
index 03b3ad798f..988bf72541 100644
--- a/platypush/entities/weather.py
+++ b/platypush/entities/weather.py
@@ -1,4 +1,4 @@
-from sqlalchemy import Column, DateTime, Float, Integer, ForeignKey, String
+from sqlalchemy import JSON, Column, DateTime, Float, Integer, ForeignKey, String
from . import Entity
@@ -35,3 +35,20 @@ class Weather(Entity):
__mapper_args__ = {
'polymorphic_identity': __tablename__,
}
+
+
+class WeatherForecast(Entity):
+ """
+ Weather forecast entity.
+ """
+
+ __tablename__ = 'weather_forecast'
+
+ id = Column(Integer, ForeignKey(Entity.id, ondelete='CASCADE'), primary_key=True)
+ # forecast contains a list of serialized Weather entities
+ forecast = Column(JSON)
+
+ __table_args__ = {'extend_existing': True}
+ __mapper_args__ = {
+ 'polymorphic_identity': __tablename__,
+ }
diff --git a/platypush/message/event/weather.py b/platypush/message/event/weather.py
index 5aefdb4c99..e4a27f8000 100644
--- a/platypush/message/event/weather.py
+++ b/platypush/message/event/weather.py
@@ -1,5 +1,5 @@
from datetime import datetime
-from typing import Optional
+from typing import List, Optional
from platypush.message.event import Event
@@ -104,4 +104,30 @@ class NewPrecipitationForecastEvent(Event):
)
+class NewWeatherForecastEvent(Event):
+ """
+ Event triggered when a new weather forecast is received.
+ """
+
+ def __init__(
+ self,
+ *args,
+ plugin_name: str,
+ forecast: List[dict],
+ **kwargs,
+ ):
+ """
+ :param forecast: List of weather forecast items. Format:
+
+ .. schema:: weather.openweathermap.WeatherSchema(many=True)
+
+ """
+ super().__init__(
+ *args,
+ plugin_name=plugin_name,
+ forecast=forecast,
+ **kwargs,
+ )
+
+
# vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/weather/__init__.py b/platypush/plugins/weather/__init__.py
index c3bd3064a3..e4fb85f73d 100644
--- a/platypush/plugins/weather/__init__.py
+++ b/platypush/plugins/weather/__init__.py
@@ -1,9 +1,13 @@
from abc import ABC, abstractmethod
-from typing import Optional
+from typing import List, Optional
from platypush.entities.managers.weather import WeatherEntityManager
-from platypush.message.event.weather import NewWeatherConditionEvent
+from platypush.message.event.weather import (
+ NewWeatherConditionEvent,
+ NewWeatherForecastEvent,
+)
from platypush.plugins import RunnablePlugin, action
+from platypush.schemas.weather.openweathermap import WeatherReportSchema
from platypush.utils import get_plugin_name_by_class
@@ -15,6 +19,7 @@ class WeatherPlugin(RunnablePlugin, WeatherEntityManager, ABC):
def __init__(self, poll_interval: Optional[float] = 120, **kwargs):
super().__init__(poll_interval=poll_interval, **kwargs)
self._latest_weather = None
+ self._latest_forecast = None
def _on_weather_data(self, weather: dict, always_publish: bool = False):
if weather != self._latest_weather or always_publish:
@@ -24,42 +29,99 @@ class WeatherPlugin(RunnablePlugin, WeatherEntityManager, ABC):
)
)
- self.publish_entities([weather])
+ self.publish_entities([weather], type='weather')
self._latest_weather = weather
+ def _on_weather_forecast(self, forecast: List[dict], always_publish: bool = False):
+ if forecast != self._latest_forecast or always_publish:
+ self._bus.post(
+ NewWeatherForecastEvent(
+ plugin_name=get_plugin_name_by_class(self.__class__),
+ forecast=forecast,
+ )
+ )
+
+ self.publish_entities(forecast, type='forecast')
+
+ self._latest_forecast = forecast
+
@action
- def get_current_weather(self, *args, **kwargs) -> dict:
- weather = self._get_current_weather(*args, **kwargs)
+ def get_current_weather(
+ self,
+ *args,
+ lat: Optional[float] = None,
+ long: Optional[float] = None,
+ units: Optional[str] = None,
+ **kwargs
+ ) -> dict:
+ """
+ Returns the current weather.
+
+ :param lat: Override the ``lat`` configuration value.
+ :param long: Override the ``long`` configuration value.
+ :param units: Override the ``units`` configuration value.
+ :return: .. schema:: weather.openweathermap.WeatherSchema
+ """
+ weather = self._get_current_weather(
+ *args, lat=lat, long=long, units=units, **kwargs
+ )
self._on_weather_data(weather, always_publish=True)
return weather
@action
- def status(self, *args, **kwargs):
+ def get_forecast(
+ self,
+ *args,
+ lat: Optional[float] = None,
+ long: Optional[float] = None,
+ units: Optional[str] = None,
+ **kwargs
+ ) -> List[dict]:
"""
- Alias for :meth:`get_current_weather`.
+ Returns the weather forecast for the upcoming hours/days.
+
+ :param lat: Override the ``lat`` configuration value.
+ :param long: Override the ``long`` configuration value.
+ :param units: Override the ``units`` configuration value.
+ :return: .. schema:: weather.openweathermap.WeatherSchema(many=True)
"""
- return self.get_current_weather(*args, **kwargs)
+ forecast = self._get_forecast(*args, lat=lat, long=long, units=units, **kwargs)
+
+ if forecast:
+ self._on_weather_forecast(forecast, always_publish=True)
+
+ return forecast
+
+ @action
+ def status(self, *args, **kwargs) -> dict:
+ """
+ :return: .. schema:: weather.openweathermap.WeatherReportSchema
+ """
+ return self._status(*args, **kwargs)
+
+ def _status(self, *args, **kwargs) -> dict:
+ return dict(
+ WeatherReportSchema().dump(
+ {
+ 'current': self.get_current_weather(*args, **kwargs).output,
+ 'forecast': self.get_forecast(*args, **kwargs).output,
+ }
+ )
+ )
@abstractmethod
def _get_current_weather(self, *args, **kwargs) -> dict:
raise NotImplementedError("_get_current_weather not implemented")
+ @abstractmethod
+ def _get_forecast(self, *args, **kwargs) -> List[dict]:
+ raise NotImplementedError("_get_forecast not implemented")
+
def main(self):
while not self.should_stop():
try:
- current_weather = self._get_current_weather() or {}
- 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
+ self._status()
except Exception as e:
self.logger.exception(e)
finally:
diff --git a/platypush/plugins/weather/openweathermap/__init__.py b/platypush/plugins/weather/openweathermap/__init__.py
index 64319d8e3e..f0455b7880 100644
--- a/platypush/plugins/weather/openweathermap/__init__.py
+++ b/platypush/plugins/weather/openweathermap/__init__.py
@@ -1,4 +1,4 @@
-from typing import Optional
+from typing import List, Optional
import requests
@@ -14,7 +14,7 @@ class WeatherOpenweathermapPlugin(WeatherPlugin): # pylint: disable=too-many-an
`_ 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'
def __init__(
self,
@@ -25,7 +25,8 @@ class WeatherOpenweathermapPlugin(WeatherPlugin): # pylint: disable=too-many-an
long: Optional[float] = None,
zip_code: Optional[str] = None,
units: str = 'metric',
- **kwargs
+ lang: Optional[str] = None,
+ **kwargs,
):
"""
:param token: OpenWeatherMap API token.
@@ -44,8 +45,7 @@ class WeatherOpenweathermapPlugin(WeatherPlugin): # pylint: disable=too-many-an
``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.
+ :param lang: Language code for the weather description (default: en).
"""
super().__init__(**kwargs)
self._token = token
@@ -54,6 +54,7 @@ class WeatherOpenweathermapPlugin(WeatherPlugin): # pylint: disable=too-many-an
location=location, city_id=city_id, lat=lat, long=long, zip_code=zip_code
)
self.units = units
+ self.lang = lang
def _get_location_query(
self,
@@ -75,8 +76,9 @@ class WeatherOpenweathermapPlugin(WeatherPlugin): # pylint: disable=too-many-an
assert self._location_query, 'Specify either location, city_id or lat/long'
return self._location_query
- def _get_current_weather(
+ def _weather_request(
self,
+ path: str,
*_,
location: Optional[str] = None,
city_id: Optional[int] = None,
@@ -84,23 +86,13 @@ class WeatherOpenweathermapPlugin(WeatherPlugin): # pylint: disable=too-many-an
long: Optional[float] = None,
zip_code: Optional[str] = None,
units: Optional[str] = None,
- **__
+ **__,
) -> dict:
- """
- Returns the current weather.
-
- :param location: Override the ``location`` configuration value.
- :param city_id: Override the ``city_id`` configuration value.
- :param lat: Override the ``lat`` configuration value.
- :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
- """
units = units or self.units
params = {
'appid': self._token,
'units': units,
+ 'lang': self.lang,
**self._get_location_query(
location=location,
city_id=city_id,
@@ -110,8 +102,68 @@ class WeatherOpenweathermapPlugin(WeatherPlugin): # pylint: disable=too-many-an
),
}
- rs = requests.get(self.base_url, params=params, timeout=10)
+ rs = requests.get(f'{self.base_url}/{path}', params=params, timeout=10)
rs.raise_for_status()
- state = rs.json()
- state['units'] = units
- return dict(WeatherSchema().dump(state))
+ return rs.json()
+
+ 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:
+ units = units or self.units
+ return dict(
+ WeatherSchema().dump(
+ {
+ 'units': units,
+ **self._weather_request(
+ 'weather',
+ location=location,
+ city_id=city_id,
+ lat=lat,
+ long=long,
+ zip_code=zip_code,
+ units=units,
+ ),
+ }
+ )
+ )
+
+ def _get_forecast(
+ 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,
+ **__,
+ ) -> List[dict]:
+ units = units or self.units
+ return list(
+ WeatherSchema().dump(
+ [
+ {
+ 'units': units,
+ **data,
+ }
+ for data in self._weather_request(
+ 'forecast',
+ location=location,
+ city_id=city_id,
+ lat=lat,
+ long=long,
+ zip_code=zip_code,
+ units=units,
+ ).get('list', [])
+ ],
+ many=True,
+ )
+ )
diff --git a/platypush/schemas/weather/openweathermap.py b/platypush/schemas/weather/openweathermap.py
index ce88a5ba94..10a3675e08 100644
--- a/platypush/schemas/weather/openweathermap.py
+++ b/platypush/schemas/weather/openweathermap.py
@@ -22,6 +22,15 @@ class WeatherSchema(Schema):
Schema for weather data.
"""
+ time = DateTime(
+ required=True,
+ attribute='dt',
+ metadata={
+ 'description': 'Time of the weather condition',
+ 'example': '2020-01-01T12:00:00+00:00',
+ },
+ )
+
summary = fields.Function(
lambda obj: obj.get('weather', [{'main': 'Unknown'}])[0].get('main', 'Unknown'),
metadata={
@@ -164,3 +173,12 @@ class WeatherSchema(Schema):
data['sunset'] = self._timestamp_to_dt(sunset)
return data
+
+
+class WeatherReportSchema(Schema):
+ """
+ Schema for full weather reports.
+ """
+
+ current = fields.Nested(WeatherSchema, required=True)
+ forecast = fields.List(fields.Nested(WeatherSchema), required=True)