diff --git a/platypush/backend/http/webapp/src/assets/icons.json b/platypush/backend/http/webapp/src/assets/icons.json
index 3a481c13a..f6f254404 100644
--- a/platypush/backend/http/webapp/src/assets/icons.json
+++ b/platypush/backend/http/webapp/src/assets/icons.json
@@ -134,6 +134,9 @@
"variable": {
"class": "fas fa-square-root-variable"
},
+ "weather.openweathermap": {
+ "class": "fas fa-cloud-sun-rain"
+ },
"zigbee.mqtt": {
"imgUrl": "/icons/zigbee.svg"
},
diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/Weather.vue b/platypush/backend/http/webapp/src/components/panels/Entities/Weather.vue
new file mode 100644
index 000000000..098c0de48
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/Weather.vue
@@ -0,0 +1,277 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 fb7fd0465..b4d89cd52 100644
--- a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
@@ -15,6 +15,14 @@
}
},
+ "weather": {
+ "name": "Weather",
+ "name_plural": "Weather",
+ "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
new file mode 100644
index 000000000..f973c884e
--- /dev/null
+++ b/platypush/entities/managers/weather.py
@@ -0,0 +1,49 @@
+from abc import ABC, abstractmethod
+from typing import List
+
+from platypush.utils import get_plugin_name_by_class
+
+from . import EntityManager
+
+
+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
+
+ 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'),
+ 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'),
+ 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'),
+ )
+ ]
+ )
+
+ @abstractmethod
+ def status(self, *_, **__):
+ raise NotImplementedError
diff --git a/platypush/entities/weather.py b/platypush/entities/weather.py
new file mode 100644
index 000000000..93ccd90d5
--- /dev/null
+++ b/platypush/entities/weather.py
@@ -0,0 +1,35 @@
+from sqlalchemy import Column, DateTime, Float, Integer, ForeignKey, String
+
+from . import Entity
+
+
+class Weather(Entity):
+ """
+ Weather entity.
+ """
+
+ __tablename__ = 'weather'
+
+ id = Column(Integer, ForeignKey(Entity.id, ondelete='CASCADE'), primary_key=True)
+
+ summary = Column(String)
+ icon = Column(String)
+ precip_intensity = Column(Float)
+ precip_type = Column(String)
+ temperature = Column(Float)
+ apparent_temperature = Column(Float)
+ humidity = Column(Float)
+ pressure = Column(Float)
+ wind_speed = Column(Float)
+ wind_direction = Column(Float)
+ wind_gust = Column(Float)
+ cloud_cover = Column(Float)
+ visibility = Column(Float)
+ sunrise = Column(DateTime)
+ sunset = Column(DateTime)
+ units = Column(String)
+
+ __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 47dfd39fe..9dbb9a110 100644
--- a/platypush/message/event/weather.py
+++ b/platypush/message/event/weather.py
@@ -28,6 +28,7 @@ class NewWeatherConditionEvent(Event):
visibility: Optional[float] = None,
sunrise: Optional[datetime] = None,
sunset: Optional[datetime] = None,
+ units: str = 'metric',
**kwargs,
):
"""
@@ -48,6 +49,7 @@ class NewWeatherConditionEvent(Event):
:param visibility: Visibility, in meters.
:param sunrise: Sunrise time.
:param sunset: Sunset time.
+ :param units: Unit system (default: metric).
"""
super().__init__(
*args,
@@ -67,6 +69,7 @@ class NewWeatherConditionEvent(Event):
visibility=visibility,
sunrise=sunrise,
sunset=sunset,
+ units=units,
**kwargs,
)
diff --git a/platypush/plugins/weather/__init__.py b/platypush/plugins/weather/__init__.py
index dca429182..c3bd3064a 100644
--- a/platypush/plugins/weather/__init__.py
+++ b/platypush/plugins/weather/__init__.py
@@ -1,12 +1,13 @@
from abc import ABC, abstractmethod
from typing import Optional
+from platypush.entities.managers.weather import WeatherEntityManager
from platypush.message.event.weather import NewWeatherConditionEvent
from platypush.plugins import RunnablePlugin, action
from platypush.utils import get_plugin_name_by_class
-class WeatherPlugin(RunnablePlugin, ABC):
+class WeatherPlugin(RunnablePlugin, WeatherEntityManager, ABC):
"""
Base class for weather plugins.
"""
@@ -15,15 +16,39 @@ class WeatherPlugin(RunnablePlugin, ABC):
super().__init__(poll_interval=poll_interval, **kwargs)
self._latest_weather = None
+ def _on_weather_data(self, weather: dict, always_publish: bool = False):
+ if weather != self._latest_weather or always_publish:
+ self._bus.post(
+ NewWeatherConditionEvent(
+ plugin_name=get_plugin_name_by_class(self.__class__), **weather
+ )
+ )
+
+ self.publish_entities([weather])
+
+ self._latest_weather = weather
+
@action
+ def get_current_weather(self, *args, **kwargs) -> dict:
+ weather = self._get_current_weather(*args, **kwargs)
+ self._on_weather_data(weather, always_publish=True)
+ return weather
+
+ @action
+ def status(self, *args, **kwargs):
+ """
+ Alias for :meth:`get_current_weather`.
+ """
+ return self.get_current_weather(*args, **kwargs)
+
@abstractmethod
- def get_current_weather(self, *args, **kwargs):
- raise NotImplementedError("get_current_weather not implemented")
+ def _get_current_weather(self, *args, **kwargs) -> dict:
+ 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 = self._get_current_weather() or {}
current_weather.pop("time", None)
if current_weather != self._latest_weather:
diff --git a/platypush/plugins/weather/openweathermap/__init__.py b/platypush/plugins/weather/openweathermap/__init__.py
index 86438ba7e..64319d8e3 100644
--- a/platypush/plugins/weather/openweathermap/__init__.py
+++ b/platypush/plugins/weather/openweathermap/__init__.py
@@ -2,12 +2,11 @@ from typing import Optional
import requests
-from platypush.plugins import action
from platypush.plugins.weather import WeatherPlugin
from platypush.schemas.weather.openweathermap import WeatherSchema
-class WeatherOpenweathermapPlugin(WeatherPlugin):
+class WeatherOpenweathermapPlugin(WeatherPlugin): # pylint: disable=too-many-ancestors
"""
OpenWeatherMap plugin.
@@ -76,8 +75,7 @@ class WeatherOpenweathermapPlugin(WeatherPlugin):
assert self._location_query, 'Specify either location, city_id or lat/long'
return self._location_query
- @action
- def get_current_weather(
+ def _get_current_weather(
self,
*_,
location: Optional[str] = None,
@@ -99,9 +97,10 @@ class WeatherOpenweathermapPlugin(WeatherPlugin):
:param units: Override the ``units`` configuration value.
:return: .. schema:: weather.openweathermap.WeatherSchema
"""
+ units = units or self.units
params = {
'appid': self._token,
- 'units': units or self.units,
+ 'units': units,
**self._get_location_query(
location=location,
city_id=city_id,
@@ -113,4 +112,6 @@ class WeatherOpenweathermapPlugin(WeatherPlugin):
rs = requests.get(self.base_url, params=params, timeout=10)
rs.raise_for_status()
- return dict(WeatherSchema().dump(rs.json()))
+ state = rs.json()
+ state['units'] = units
+ return dict(WeatherSchema().dump(state))
diff --git a/platypush/schemas/weather/openweathermap.py b/platypush/schemas/weather/openweathermap.py
index fab56e8af..ce88a5ba9 100644
--- a/platypush/schemas/weather/openweathermap.py
+++ b/platypush/schemas/weather/openweathermap.py
@@ -1,6 +1,10 @@
from typing import Optional
+
+from datetime import datetime
+from dateutil.tz import tzutc
from marshmallow import fields, pre_dump
from marshmallow.schema import Schema
+from marshmallow.validate import OneOf
from platypush.schemas import DateTime
@@ -135,6 +139,19 @@ class WeatherSchema(Schema):
},
)
+ units = fields.String(
+ missing='metric',
+ validate=OneOf(['metric', 'imperial']),
+ metadata={
+ 'description': 'Unit of measure',
+ 'example': 'metric',
+ },
+ )
+
+ @staticmethod
+ def _timestamp_to_dt(timestamp: float) -> datetime:
+ return datetime.fromtimestamp(timestamp, tz=tzutc())
+
@pre_dump
def _pre_dump(self, data: dict, **_) -> dict:
sun_data = data.pop('sys', {})
@@ -142,8 +159,8 @@ class WeatherSchema(Schema):
sunset = sun_data.get('sunset')
if sunrise is not None:
- data['sunrise'] = sunrise
+ data['sunrise'] = self._timestamp_to_dt(sunrise)
if sunset is not None:
- data['sunset'] = sunset
+ data['sunset'] = self._timestamp_to_dt(sunset)
return data