forked from platypush/platypush
Added support for weather forecast events and entities.
This commit is contained in:
parent
841a28066b
commit
b969afb1cf
10 changed files with 416 additions and 96 deletions
|
@ -2,28 +2,16 @@
|
||||||
<div class="entity weather-container">
|
<div class="entity weather-container">
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<div class="col-1 icon">
|
<div class="col-1 icon">
|
||||||
<EntityIcon
|
<WeatherIcon :value="value" />
|
||||||
:entity="value"
|
|
||||||
:loading="loading"
|
|
||||||
:error="error" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-5 name">
|
<div class="col-5 name">
|
||||||
<div class="name" v-text="value.name" />
|
<div class="name" v-text="formatDateTime(value.time, year=false, seconds=false)" v-if="isForecast" />
|
||||||
|
<div class="name" v-text="value.name" v-else />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-5 current-weather" @click.stop="isCollapsed = !isCollapsed">
|
<div class="col-5 current-weather" @click.stop="isCollapsed = !isCollapsed">
|
||||||
<div class="weather-summary">
|
<div class="weather-summary">
|
||||||
<img :src="`/icons/openweathermap/dark/${value.icon}.png`"
|
|
||||||
:alt="value?.summary"
|
|
||||||
class="weather-icon"
|
|
||||||
v-if="value.icon" />
|
|
||||||
|
|
||||||
<img :src="value.image"
|
|
||||||
:alt="value?.summary"
|
|
||||||
class="weather-icon"
|
|
||||||
v-else-if="value.image" />
|
|
||||||
|
|
||||||
<span class="temperature"
|
<span class="temperature"
|
||||||
v-text="normTemperature"
|
v-text="normTemperature"
|
||||||
v-if="normTemperature != null" />
|
v-if="normTemperature != null" />
|
||||||
|
@ -184,11 +172,20 @@
|
||||||
<script>
|
<script>
|
||||||
import EntityIcon from "./EntityIcon"
|
import EntityIcon from "./EntityIcon"
|
||||||
import EntityMixin from "./EntityMixin"
|
import EntityMixin from "./EntityMixin"
|
||||||
|
import WeatherIcon from "./WeatherIcon"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {EntityIcon},
|
components: {EntityIcon, WeatherIcon},
|
||||||
mixins: [EntityMixin],
|
mixins: [EntityMixin],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: Object,
|
||||||
|
isForecast: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
|
@ -276,13 +273,6 @@ export default {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather-icon {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temperature {
|
.temperature {
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
<template>
|
||||||
|
<div class="entity weather-forecast-container">
|
||||||
|
<div class="head">
|
||||||
|
<div class="col-1 icon">
|
||||||
|
<WeatherIcon :value="firstForecast" v-if="firstForecast" />
|
||||||
|
<EntityIcon
|
||||||
|
:entity="value"
|
||||||
|
:loading="loading"
|
||||||
|
:error="error"
|
||||||
|
v-else />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-5 name" @click.stop="isCollapsed = !isCollapsed">
|
||||||
|
<div class="name" v-text="value.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-5 summary-container" @click.stop="isCollapsed = !isCollapsed">
|
||||||
|
<div class="summary">
|
||||||
|
<span class="temperature"
|
||||||
|
v-text="normTemperature"
|
||||||
|
v-if="normTemperature != null" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-1 collapse-toggler" @click.stop="isCollapsed = !isCollapsed">
|
||||||
|
<i class="fas"
|
||||||
|
:class="{'fa-chevron-down': isCollapsed, 'fa-chevron-up': !isCollapsed}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body children attributes fade-in" v-if="!isCollapsed">
|
||||||
|
<div class="child" v-for="weather in value.forecast" :key="weather.time">
|
||||||
|
<Weather :value="weather" :is-forecast="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EntityIcon from "./EntityIcon"
|
||||||
|
import EntityMixin from "./EntityMixin"
|
||||||
|
import Weather from "./Weather"
|
||||||
|
import WeatherIcon from "./WeatherIcon"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {EntityIcon, Weather, WeatherIcon},
|
||||||
|
mixins: [EntityMixin],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isCollapsed: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
firstForecast() {
|
||||||
|
return this.value?.forecast?.[0]
|
||||||
|
},
|
||||||
|
|
||||||
|
normTemperature() {
|
||||||
|
if (this.firstForecast?.temperature == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return Math.round(this.firstForecast.temperature).toFixed(1) + "°"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.weather-forecast-container {
|
||||||
|
.body {
|
||||||
|
padding-top: 0;
|
||||||
|
max-height: 35em;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.child {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 -0.5em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $hover-bg !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.25em;
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<span class="entity weather-icon-container">
|
||||||
|
<img :src="`/icons/openweathermap/dark/${value.icon}.png`"
|
||||||
|
:alt="value?.summary"
|
||||||
|
class="weather-icon"
|
||||||
|
v-if="value.icon" />
|
||||||
|
|
||||||
|
<img :src="value.image"
|
||||||
|
:alt="value?.summary"
|
||||||
|
class="weather-icon"
|
||||||
|
v-else-if="value.image" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Object,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.weather-icon-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 2.5em;
|
||||||
|
|
||||||
|
.weather-icon {
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0.5em 0.5em 0.5em 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -23,6 +23,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"weather_forecast": {
|
||||||
|
"name": "Weather Forecast",
|
||||||
|
"name_plural": "Weather Forecast",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-cloud-sun-rain"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"button": {
|
"button": {
|
||||||
"name": "Button",
|
"name": "Button",
|
||||||
"name_plural": "Buttons",
|
"name_plural": "Buttons",
|
||||||
|
|
|
@ -11,41 +11,42 @@ class WeatherEntityManager(EntityManager, ABC):
|
||||||
Base class for integrations that support weather reports.
|
Base class for integrations that support weather reports.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def transform_entities(self, entities: List[dict]):
|
def transform_entities(self, entities: List[dict], *, type: str):
|
||||||
from platypush.entities.weather import Weather
|
from platypush.entities.weather import Weather, WeatherForecast
|
||||||
|
|
||||||
if not entities:
|
if not entities:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
weather = entities[0]
|
|
||||||
plugin = get_plugin_name_by_class(self.__class__)
|
plugin = get_plugin_name_by_class(self.__class__)
|
||||||
|
|
||||||
|
if type == 'weather':
|
||||||
|
# Current weather response
|
||||||
|
weather = entities[0]
|
||||||
|
weather.pop('time', None)
|
||||||
return super().transform_entities(
|
return super().transform_entities(
|
||||||
[
|
[
|
||||||
Weather(
|
Weather(
|
||||||
id=plugin,
|
id=f'{plugin}:weather',
|
||||||
name='Weather',
|
name='Weather',
|
||||||
summary=weather.get('summary'),
|
**weather,
|
||||||
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'),
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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
|
@abstractmethod
|
||||||
def status(self, *_, **__):
|
def status(self, *_, **__):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
|
@ -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
|
from . import Entity
|
||||||
|
|
||||||
|
@ -35,3 +35,20 @@ class Weather(Entity):
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {
|
||||||
'polymorphic_identity': __tablename__,
|
'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__,
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from platypush.message.event import Event
|
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:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
from platypush.entities.managers.weather import WeatherEntityManager
|
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.plugins import RunnablePlugin, action
|
||||||
|
from platypush.schemas.weather.openweathermap import WeatherReportSchema
|
||||||
from platypush.utils import get_plugin_name_by_class
|
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):
|
def __init__(self, poll_interval: Optional[float] = 120, **kwargs):
|
||||||
super().__init__(poll_interval=poll_interval, **kwargs)
|
super().__init__(poll_interval=poll_interval, **kwargs)
|
||||||
self._latest_weather = None
|
self._latest_weather = None
|
||||||
|
self._latest_forecast = None
|
||||||
|
|
||||||
def _on_weather_data(self, weather: dict, always_publish: bool = False):
|
def _on_weather_data(self, weather: dict, always_publish: bool = False):
|
||||||
if weather != self._latest_weather or always_publish:
|
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
|
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
|
@action
|
||||||
def get_current_weather(self, *args, **kwargs) -> dict:
|
def get_current_weather(
|
||||||
weather = self._get_current_weather(*args, **kwargs)
|
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)
|
self._on_weather_data(weather, always_publish=True)
|
||||||
return weather
|
return weather
|
||||||
|
|
||||||
@action
|
@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
|
@abstractmethod
|
||||||
def _get_current_weather(self, *args, **kwargs) -> dict:
|
def _get_current_weather(self, *args, **kwargs) -> dict:
|
||||||
raise NotImplementedError("_get_current_weather not implemented")
|
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):
|
def main(self):
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
try:
|
try:
|
||||||
current_weather = self._get_current_weather() or {}
|
self._status()
|
||||||
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:
|
except Exception as e:
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
finally:
|
finally:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ class WeatherOpenweathermapPlugin(WeatherPlugin): # pylint: disable=too-many-an
|
||||||
<https://openweathermap.org/api>`_ in order to use this API.
|
<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'
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -25,7 +25,8 @@ class WeatherOpenweathermapPlugin(WeatherPlugin): # pylint: disable=too-many-an
|
||||||
long: Optional[float] = None,
|
long: Optional[float] = None,
|
||||||
zip_code: Optional[str] = None,
|
zip_code: Optional[str] = None,
|
||||||
units: str = 'metric',
|
units: str = 'metric',
|
||||||
**kwargs
|
lang: Optional[str] = None,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param token: OpenWeatherMap API token.
|
: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.
|
``zip,country_code``) will be used by default for weather lookup.
|
||||||
:param units: Supported: ``metric`` (default), ``standard`` and
|
:param units: Supported: ``metric`` (default), ``standard`` and
|
||||||
``imperial``.
|
``imperial``.
|
||||||
:param poll_interval: How often the weather should be refreshed, in
|
:param lang: Language code for the weather description (default: en).
|
||||||
seconds.
|
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._token = token
|
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
|
location=location, city_id=city_id, lat=lat, long=long, zip_code=zip_code
|
||||||
)
|
)
|
||||||
self.units = units
|
self.units = units
|
||||||
|
self.lang = lang
|
||||||
|
|
||||||
def _get_location_query(
|
def _get_location_query(
|
||||||
self,
|
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'
|
assert self._location_query, 'Specify either location, city_id or lat/long'
|
||||||
return self._location_query
|
return self._location_query
|
||||||
|
|
||||||
def _get_current_weather(
|
def _weather_request(
|
||||||
self,
|
self,
|
||||||
|
path: str,
|
||||||
*_,
|
*_,
|
||||||
location: Optional[str] = None,
|
location: Optional[str] = None,
|
||||||
city_id: Optional[int] = None,
|
city_id: Optional[int] = None,
|
||||||
|
@ -84,23 +86,13 @@ class WeatherOpenweathermapPlugin(WeatherPlugin): # pylint: disable=too-many-an
|
||||||
long: Optional[float] = None,
|
long: Optional[float] = None,
|
||||||
zip_code: Optional[str] = None,
|
zip_code: Optional[str] = None,
|
||||||
units: Optional[str] = None,
|
units: Optional[str] = None,
|
||||||
**__
|
**__,
|
||||||
) -> dict:
|
) -> 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
|
units = units or self.units
|
||||||
params = {
|
params = {
|
||||||
'appid': self._token,
|
'appid': self._token,
|
||||||
'units': units,
|
'units': units,
|
||||||
|
'lang': self.lang,
|
||||||
**self._get_location_query(
|
**self._get_location_query(
|
||||||
location=location,
|
location=location,
|
||||||
city_id=city_id,
|
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()
|
rs.raise_for_status()
|
||||||
state = rs.json()
|
return rs.json()
|
||||||
state['units'] = units
|
|
||||||
return dict(WeatherSchema().dump(state))
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -22,6 +22,15 @@ class WeatherSchema(Schema):
|
||||||
Schema for weather data.
|
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(
|
summary = fields.Function(
|
||||||
lambda obj: obj.get('weather', [{'main': 'Unknown'}])[0].get('main', 'Unknown'),
|
lambda obj: obj.get('weather', [{'main': 'Unknown'}])[0].get('main', 'Unknown'),
|
||||||
metadata={
|
metadata={
|
||||||
|
@ -164,3 +173,12 @@ class WeatherSchema(Schema):
|
||||||
data['sunset'] = self._timestamp_to_dt(sunset)
|
data['sunset'] = self._timestamp_to_dt(sunset)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherReportSchema(Schema):
|
||||||
|
"""
|
||||||
|
Schema for full weather reports.
|
||||||
|
"""
|
||||||
|
|
||||||
|
current = fields.Nested(WeatherSchema, required=True)
|
||||||
|
forecast = fields.List(fields.Nested(WeatherSchema), required=True)
|
||||||
|
|
Loading…
Reference in a new issue