From b8a4b9e4c54fb9de7e7ddca30cacdd0d58445297 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Mon, 20 Nov 2023 01:46:01 +0100 Subject: [PATCH] Implemented support for weather entities. --- .../backend/http/webapp/src/assets/icons.json | 3 + .../components/panels/Entities/Weather.vue | 277 ++++++++++++++++++ .../src/components/panels/Entities/meta.json | 8 + platypush/entities/managers/weather.py | 49 ++++ platypush/entities/weather.py | 35 +++ platypush/message/event/weather.py | 3 + platypush/plugins/weather/__init__.py | 33 ++- .../weather/openweathermap/__init__.py | 13 +- platypush/schemas/weather/openweathermap.py | 21 +- 9 files changed, 430 insertions(+), 12 deletions(-) create mode 100644 platypush/backend/http/webapp/src/components/panels/Entities/Weather.vue create mode 100644 platypush/entities/managers/weather.py create mode 100644 platypush/entities/weather.py diff --git a/platypush/backend/http/webapp/src/assets/icons.json b/platypush/backend/http/webapp/src/assets/icons.json index 3a481c13a2..f6f2544040 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 0000000000..098c0de485 --- /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 fb7fd0465c..b4d89cd52b 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 0000000000..f973c884e2 --- /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 0000000000..93ccd90d5d --- /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 47dfd39fe8..9dbb9a110d 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 dca429182b..c3bd3064a3 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 86438ba7eb..64319d8e3e 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 fab56e8afa..ce88a5ba94 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