-
Notifications
You must be signed in to change notification settings - Fork 19
weather panel #292
weather panel #292
Changes from all commits
150adef
f38dbcb
5fc2f32
2b52c57
8c2b78f
3ac3494
6e3b775
e6cff3f
5f36842
d615a91
3a37d5c
253369d
5fe1d91
9dcb33f
f1fee5a
994e47a
e906f26
3231644
8ba3cf1
5dfe82b
1794f56
3fd7e5b
bbd9f4e
1186d34
ded786b
5a60d86
3a2affa
c875c3f
30a80bf
f76c61b
4fdc96c
5b5ba19
618ad60
5fceea4
cbe4258
299d9e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import time | ||
|
||
from django.core.management import base | ||
|
||
from piplmesh.panels.weather import tasks | ||
|
||
class Command(base.BaseCommand): | ||
help = 'Force weather update.' | ||
|
||
def handle(self, *args, **options): | ||
""" | ||
Forces Weather update. | ||
""" | ||
|
||
verbosity = int(options['verbosity']) | ||
|
||
if verbosity > 1: | ||
self.stdout.write("Updating weather...\n") | ||
|
||
results = tasks.generate_weather_tasks() | ||
prev_completed = None | ||
while not results.ready(): | ||
completed = results.completed_count() | ||
if prev_completed != completed: | ||
self.stdout.write("Completed %d/%d.\n" % (completed, len(results))) | ||
prev_completed = completed | ||
time.sleep(1) | ||
|
||
if verbosity > 1: | ||
self.stdout.write("Successfully updated weather.\n") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from django.utils.translation import ugettext_lazy as _ | ||
|
||
import mongoengine | ||
|
||
SYMBOLS = ( | ||
(1, _("Sun")), | ||
(2, _("Light cloud")), | ||
(3, _("Partly cloud")), | ||
(4, _("Cloud")), | ||
(5, _("Light rain and sun")), | ||
(6, _("Light rain, thunder and sun")), | ||
(7, _("Sleet and sun")), | ||
(8, _("Snow and sun")), | ||
(9, _("Light rain")), | ||
(10, _("Rain")), | ||
(11, _("Rain and thunder")), | ||
(12, _("Sleet")), | ||
(13, _("Snow")), | ||
(14, _("Snow and thunder")), | ||
(15, _("Fog")), | ||
(16, _("Sun (used for winter darkness)")), | ||
(17, _("Light cloud ( winter darkness )")), | ||
(18, _("Light rain and sun")), | ||
(19, _("Snow and sun ( used for winter darkness )")), | ||
(20, _("Sleet, sun and thunder")), | ||
(21, _("Snow, sun and thunder")), | ||
(22, _("Light rain and thunder")), | ||
(23, _("Sleet thunder")), | ||
) | ||
|
||
# TODO: Some fields have to be unique to make sure that there is only one entry for each time period for given location. | ||
# https://github.com/wlanslovenija/PiplMesh/issues/210 | ||
class Weather(mongoengine.Document): | ||
created = mongoengine.DateTimeField(required=True) | ||
latitude = mongoengine.DecimalField(required=True) | ||
longitude = mongoengine.DecimalField(required=True) | ||
model_name = mongoengine.StringField(required=True) | ||
|
||
meta = { | ||
'allow_inheritance': True, | ||
} | ||
|
||
class Precipitation(Weather): | ||
date_from = mongoengine.DateTimeField(required=True) | ||
date_to = mongoengine.DateTimeField(required=True) | ||
precipitation = mongoengine.DecimalField(required=True) | ||
symbol = mongoengine.IntField(choices=SYMBOLS, required=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You have here IntField, but above you have strings as keys, not numbers. Does this work? |
||
|
||
class State(Weather): | ||
at = mongoengine.DateTimeField(required=True) | ||
temperature = mongoengine.DecimalField(required=True) | ||
wind_direction = mongoengine.StringField(required=True) | ||
wind_angle = mongoengine.DecimalField(required=True) | ||
wind_speed = mongoengine.DecimalField(required=True) | ||
humidity = mongoengine.DecimalField(required=True) | ||
pressure = mongoengine.DecimalField(required=True) | ||
cloudiness = mongoengine.DecimalField(required=True) | ||
fog = mongoengine.DecimalField(required=True) | ||
low_clouds = mongoengine.DecimalField(required=True) | ||
medium_clouds = mongoengine.DecimalField(required=True) | ||
high_clouds = mongoengine.DecimalField(required=True) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
from __future__ import absolute_import | ||
|
||
import datetime | ||
|
||
from django.utils import timezone, translation | ||
from django.utils.translation import ugettext_lazy as _ | ||
|
||
from piplmesh import nodes, panels | ||
|
||
from . import models | ||
|
||
WEATHER_OBSOLETE = 6 # 6 hours | ||
FORECAST_HOUR = 14 # 14:00 o'clock or 2:00 PM | ||
WEATHER_FORECAST_RANGE = 3 # days from being created | ||
|
||
class WeatherPanel(panels.BasePanel): | ||
def get_context(self, context): | ||
context.update({ | ||
'header': _("Weather"), | ||
}) | ||
|
||
latitude = self.request.node.latitude | ||
longitude = self.request.node.longitude | ||
# TODO: Check and possibly optimize | ||
state = models.State.objects(latitude=latitude, longitude=longitude, at__lte=datetime.datetime.now(), at__gte=(datetime.datetime.now() - datetime.timedelta(hours=WEATHER_OBSOLETE))).order_by('+created').first() | ||
if state is None: | ||
context.update({ | ||
'error_data': True, | ||
}) | ||
return context | ||
|
||
if timezone.now() >= state.created + datetime.timedelta(hours=WEATHER_OBSOLETE): | ||
context.update({ | ||
'error_obsolete': True, | ||
}) | ||
return context | ||
|
||
context.update({ | ||
'weather_objects': get_weather_content(latitude, longitude), | ||
'created': state.created, | ||
}) | ||
return context | ||
|
||
# TODO: Check and possibly optimize | ||
def get_weather_content(latitude, longitude): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am a bit skeptical about this function. It looks strange. And the query above. But OK, leave it as it is. Or maybe add TODO saying to check and possibility optimize. |
||
date = datetime.datetime.now() | ||
for interval in range(0, WEATHER_FORECAST_RANGE): | ||
state = models.State.objects(latitude=latitude, longitude=longitude, at__lte=date).order_by('-at').first() | ||
precipitation = models.Precipitation.objects(latitude=latitude, longitude=longitude, date_from__lte=date, date_to__gte=date).order_by('-date_from').first() | ||
weather_object = { | ||
'at': state.at, | ||
'temperature': state.temperature, | ||
'symbol': precipitation.symbol, | ||
} | ||
date += datetime.timedelta(days=1) | ||
date = date.replace(hour=FORECAST_HOUR, minute=0) | ||
yield weather_object | ||
|
||
panels.panels_pool.register(WeatherPanel) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
.error { | ||
font-size: x-small; | ||
} | ||
|
||
#weather { | ||
font-size: 10px; | ||
text-align: left; | ||
} | ||
|
||
#weather ul { | ||
float: center; | ||
padding: 0; | ||
} | ||
|
||
#weather li { | ||
display: inline; | ||
float: left; | ||
width: 100%; | ||
} | ||
|
||
#weather span { | ||
font-size: 7px; | ||
} | ||
|
||
#forecast ul { | ||
border: 0; | ||
display: table; | ||
list-style: none; | ||
margin: 0 auto; | ||
text-align: center; | ||
width: 250px; | ||
} | ||
|
||
#forecast li { | ||
border: none; | ||
display: table-cell; | ||
margin-left: 5px; | ||
padding: 0; | ||
vertical-align: middle; | ||
width: 77px; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
from __future__ import absolute_import | ||
|
||
import datetime | ||
|
||
from lxml import etree, objectify | ||
|
||
import celery | ||
from celery import task | ||
from celery.task import schedules | ||
|
||
from piplmesh import nodes | ||
from piplmesh.utils import decorators | ||
|
||
from . import models, panel | ||
|
||
CHECK_FOR_NEW_WEATHER = 30 # minutes | ||
|
||
SOURCE_URL = 'http://api.met.no/' | ||
|
||
def fetch_data(latitude, longitude): | ||
""" | ||
Get weather data for specific location | ||
""" | ||
|
||
weather_url = '%sweatherapi/locationforecast/1.8/?lat=%s;lon=%s' % (SOURCE_URL, latitude, longitude) | ||
parser = etree.XMLParser(remove_blank_text=True) | ||
lookup = objectify.ObjectifyElementClassLookup() | ||
parser.setElementClassLookup(lookup) | ||
weather = objectify.parse(weather_url, parser).getroot() | ||
return weather | ||
|
||
@task.periodic_task(run_every=schedules.crontab(minutes=CHECK_FOR_NEW_WEATHER)) | ||
@decorators.single_instance_task() | ||
def generate_weather_tasks(): | ||
""" | ||
Task which updates weather for all nodes. | ||
|
||
Obsolete data is currently left in the database. | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty line after docstring. |
||
|
||
weather_tasks = [] | ||
# Fetching data only once per possible duplicate locations | ||
for latitude, longitude in {(node.latitude, node.longitude) for node in nodes.get_all_nodes()}: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A comment discussing what we are doing here could be useful, above this line. For example:
|
||
weather_tasks.append(update_weather.s(latitude, longitude)) | ||
return celery.group(weather_tasks)() | ||
|
||
# 20 tasks per second. Limitation by the api http://api.yr.no/conditions_service.html | ||
@task.task(rate_limit=20) | ||
def update_weather(latitude, longitude): | ||
""" | ||
Task which updates weather for one location. | ||
""" | ||
|
||
weather_object = fetch_data(latitude, longitude) | ||
for product in weather_object.product.iterchildren(): | ||
if datetime.datetime.strptime(product.attrib['to'], '%Y-%m-%dT%H:%M:%SZ') < datetime.datetime.strptime(weather_object.attrib['created'], '%Y-%m-%dT%H:%M:%SZ') + datetime.timedelta(days=panel.WEATHER_FORECAST_RANGE): | ||
if product.attrib['from'] == product.attrib['to']: | ||
models.State.objects( | ||
created=datetime.datetime.strptime(weather_object.attrib['created'], '%Y-%m-%dT%H:%M:%SZ'), | ||
latitude=latitude, | ||
longitude=longitude, | ||
model_name=weather_object.meta.model.attrib['name'], | ||
at=datetime.datetime.strptime(product.attrib['from'], '%Y-%m-%dT%H:%M:%SZ') | ||
).update( | ||
set__temperature=product.location.temperature.attrib['value'], | ||
set__wind_direction=product.location.windDirection.attrib['name'], | ||
set__wind_angle=product.location.windDirection.attrib['deg'], | ||
set__wind_speed=product.location.windSpeed.attrib['mps'], | ||
set__humidity=product.location.humidity.attrib['value'], | ||
set__pressure=product.location.pressure.attrib['value'], | ||
set__cloudiness=product.location.cloudiness.attrib['percent'], | ||
set__fog=product.location.fog.attrib['percent'], | ||
set__low_clouds=product.location.lowClouds.attrib['percent'], | ||
set__medium_clouds=product.location.mediumClouds.attrib['percent'], | ||
set__high_clouds=product.location.highClouds.attrib['percent'], | ||
upsert=True | ||
) | ||
else: | ||
models.Precipitation.objects( | ||
created=datetime.datetime.strptime(weather_object.attrib['created'], '%Y-%m-%dT%H:%M:%SZ'), | ||
latitude=latitude, | ||
longitude=longitude, | ||
model_name=weather_object.meta.model.attrib['name'], | ||
date_from=datetime.datetime.strptime(product.attrib['from'], '%Y-%m-%dT%H:%M:%SZ'), | ||
date_to=datetime.datetime.strptime(product.attrib['to'], '%Y-%m-%dT%H:%M:%SZ') | ||
).update( | ||
set__precipitation=product.location.precipitation.attrib['value'], | ||
set__symbol=product.location.symbol.attrib['number'], | ||
upsert=True | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{% extends "panel/panel.html" %} | ||
|
||
{% load i18n static sekizai_tags %} | ||
|
||
{% block content %} | ||
{% addtoblock "css" %}<link href="{% static "piplmesh/panel/weather/panel.css" %}" rel="stylesheet" type="text/css" media="screen" />{% endaddtoblock %} | ||
{% if error_data or error_obsolete %} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where do you define |
||
<span class="error">{% trans "Weather data is temporarily unavailable." %}</span> | ||
{% else %} | ||
<div id="weather"> | ||
<ul> | ||
{% for weather in weather_objects %} | ||
<li id="forecast"> | ||
<ul> | ||
<li>{{ weather.at|date:"DATE_FORMAT" }}</li> | ||
<li><img src="http://api.yr.no/weatherapi/weathericon/1.0/?symbol={{ weather.symbol }};content_type=image/png" alt="{% trans "weather icon" %}" /></li> | ||
{% comment %}TODO temperature unit user-configurable{% endcomment %} | ||
<li>{{ weather.temperature }}°C</li> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a TODO comment that we should make temperature unit user-configurable. |
||
</ul> | ||
</li> | ||
{% endfor %} | ||
</ul> | ||
<p>{% blocktrans with date=created %}Forecast created on: {{ date }}{% endblocktrans %}</p> | ||
<span>{% blocktrans %}Based on data from <a href="http://met.no">met.no</a>.{% endblocktrans %}</span> | ||
</div> | ||
{% endif %} | ||
{% endblock %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
Django==1.4.1 | ||
Jinja2==2.6 | ||
PyYAML==3.10 | ||
Pygments==1.5 | ||
PyYAML==3.10 | ||
Sphinx==1.1.3 | ||
amqplib==1.0.2 | ||
anyjson==0.3.3 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty line before this.