6 from collections import defaultdict
11 import matplotlib.pyplot as plt
12 from matplotlib.backends.backend_pdf import PdfPages
14 from flask import Flask, render_template, jsonify, request, abort, Response, make_response
15 from flask_sqlalchemy import SQLAlchemy
16 from sqlalchemy import func
45 def parse_datetime(date_str):
46 return datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
51 yield from itertools.repeat(v, n)
54 def get_sqlalchemy_database_uri(config):
55 user = config.get('database', 'user')
56 pwd = config.get('database', 'password')
57 host = config.get('database', 'hostname')
58 db = config.get('database', 'database')
59 return 'mysql+mysqldb://{}:{}@{}/{}'.format(user, pwd, host, db)
62 config = configparser.ConfigParser()
63 config.read(os.environ['SEEPARKINI'])
64 apikey = config.get('openweathermap', 'apikey')
65 cityid = config.get('openweathermap', 'cityid')
66 mainsensor = config.get('webapp', 'mainsensor')
69 app.config['SQLALCHEMY_DATABASE_URI'] = get_sqlalchemy_database_uri(config)
70 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
72 with app.app_context():
76 class Sensors(db.Model):
77 __tablename__ = 'sensors'
80 class OpenWeatherMap(db.Model):
81 __tablename__ = 'openweathermap'
84 def calc_grouping_resolution(begin, end):
85 """How many data points should be between the timestamps begin and end?"""
86 # copied from munin/master/_bin/munin-cgi-graph.in
87 # except day: 300 -> 600
94 duration = (end - begin).total_seconds()
97 resolution = resolutions['day']
98 elif duration <= 7 * day:
99 resolution = resolutions['week']
100 elif duration <= 31 * day:
101 resolution = resolutions['month']
103 resolution = resolutions['year']
107 def select_sensordata(sensor_id, sensor_type, begin, end):
108 query = Sensors.query
109 if sensor_id is not None:
110 query = query.filter(Sensors.sensor_id == sensor_id)
111 if sensor_type is not None:
112 query = query.filter(Sensors.value_type == sensor_type)
113 if begin is not None:
114 query = query.filter(Sensors.timestamp >= begin)
116 query = query.filter(Sensors.timestamp <= end)
120 def sensordata_to_xy(sensordata):
121 sensordata = list(sensordata)
122 x = np.array([d.timestamp for d in sensordata])
123 y = np.array([d.value for d in sensordata])
127 def select_sensordata_grouped(sensor_id, sensor_type, begin, end):
128 # determine resolution (interval in seconds for data points)
129 resolution = calc_grouping_resolution(begin, end)
131 # Let the database do the grouping. Example in SQL (MySQL):
132 # select to_seconds(datetime) DIV (60*60*24) as interval_id, min(datetime), max(datetime), min(temp), avg(temp), max(temp), count(temp) from openweathermap group by interval_id order by interval_id;
133 query = db.session.query(func.to_seconds(Sensors.timestamp).op('div')(resolution).label('g'),
134 func.from_unixtime(func.avg(func.unix_timestamp(Sensors.timestamp))).label('timestamp'),
135 func.avg(Sensors.value).label('value'),
136 Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
137 if sensor_id is not None:
138 query = query.filter(Sensors.sensor_id == sensor_id)
139 if sensor_type is not None:
140 query = query.filter(Sensors.value_type == sensor_type)
141 query = query.filter(Sensors.timestamp >= begin)
142 query = query.filter(Sensors.timestamp <= end)
143 query = query.group_by('g', Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
147 def select_openweatherdata(cityid, begin, end):
148 query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
149 if begin is not None:
150 query = query.filter(OpenWeatherMap.datetime >= begin)
152 query = query.filter(OpenWeatherMap.datetime <= end)
156 def openweatherdata_to_xy(openweatherdata):
157 openweatherdata = list(openweatherdata)
158 x = np.array([d.datetime for d in openweatherdata])
159 y = np.array([d.temp for d in openweatherdata])
163 def select_openweatherdata_grouped(cityid, begin, end):
164 # determine resolution (interval in seconds for data points)
165 resolution = calc_grouping_resolution(begin, end)
167 # Let the database do the grouping. Example in SQL (MySQL):
168 # select to_seconds(datetime) DIV (60*60*24) as interval_id, min(datetime), max(datetime), min(temp), avg(temp), max(temp), count(temp) from openweathermap group by interval_id order by interval_id;
169 query = db.session.query(func.to_seconds(OpenWeatherMap.datetime).op('div')(resolution).label('g'),
170 func.from_unixtime(func.avg(func.unix_timestamp(OpenWeatherMap.datetime))).label('datetime'),
171 func.avg(OpenWeatherMap.temp).label('temp'),
172 OpenWeatherMap.cityid)
173 OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
174 query = query.filter(OpenWeatherMap.datetime >= begin)
175 query = query.filter(OpenWeatherMap.datetime <= end)
176 query = query.group_by('g', OpenWeatherMap.cityid)
180 def convert_to_c3(result, id, field_x, field_y):
181 c3result = defaultdict(list)
183 c3result[str(getattr(row, id))].append(getattr(row, field_y))
184 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
185 c3result[str(getattr(row, id)) + '_x'].append(dt)
189 def request_arg(key, type, default=None):
190 """Returns the key from the request if available, otherwise the default value.
191 In case type is provided and the key is present, the value is converted by calling type.
192 In other words: Reimplement request.args.get but don't return default value if
193 type raises a ValueError."""
194 if key in request.args:
196 return type(request.args[key])
197 except ValueError as e:
198 abort(Response(str(e), 400))
203 def sensordata(sensor_id=None, sensor_type=None):
204 begin = request_arg('begin', parse_datetime)
205 end = request_arg('end', parse_datetime)
206 mode = request.args.get('mode', 'full')
207 format = request.args.get('format', 'default')
210 result = select_sensordata(sensor_id, sensor_type, begin, end)
211 elif mode == 'consolidated':
212 if begin is None or end is None:
213 abort(Response('begin and end have to be set for mode==consolidated', 400))
214 result = select_sensordata_grouped(sensor_id, sensor_type, begin, end)
216 abort(Response('unknown value for mode', 400))
219 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
223 def openweathermapdata(cityid):
224 begin = request_arg('begin', parse_datetime)
225 end = request_arg('end', parse_datetime)
226 mode = request.args.get('mode', 'full')
227 format = request.args.get('format', 'default')
230 result = select_openweatherdata(cityid, begin, end)
231 elif mode == 'consolidated':
232 if begin is None or end is None:
233 abort(Response('begin and end have to be set for mode==consolidated', 400))
234 result = select_openweatherdata_grouped(cityid, begin, end)
236 abort(Response('unknown value for mode', 400))
239 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
243 def currentairtemperature(cityid):
244 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
245 return result.temp, result.datetime
248 def currentwatertemperature(sensorid):
249 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
250 return result.value, result.timestamp
253 def first_of_month(date, month):
254 date = date.replace(day=1)
258 return (date + datetime.timedelta(days=42)).replace(day=1)
260 return (date - datetime.timedelta(days=1)).replace(day=1)
264 @app.route('/api/<version>/sensors/')
265 def sensors(version):
266 """List all sensors found in the database"""
267 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
268 return jsonify(result)
271 @app.route('/api/<version>/sensor/id/<sensor_id>')
272 def sensorid(version, sensor_id):
273 """Return all data for a specific sensor
277 * ``begin=<datetime>``, optional, format like ``2018-05-19T21:07:53``
278 * ``end=<datetime>``, optional, format like ``2018-05-19T21:07:53``
279 * ``mode=<full|consolidated>``, optional. return all rows (default) or with lower resolution (for charts)
280 * ``format=<default|c3>``, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
282 result = sensordata(sensor_id=sensor_id)
283 return jsonify(result)
286 @app.route('/api/<version>/sensor/type/<sensor_type>')
287 def sensortype(version, sensor_type):
288 """Return all data for a specific sensor type
292 * ``begin=<datetime>``, optional, format like ``2018-05-19T21:07:53``
293 * ``end=<datetime>``, optional, format like ``2018-05-19T21:07:53``
294 * ``mode=<full|consolidated>``, optional. return all rows (default) or with lower resolution (for charts)
295 * ``format=<default|c3>``, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
297 result = sensordata(sensor_type=sensor_type)
298 return jsonify(result)
301 @app.route('/api/<version>/openweathermap/cities')
302 def openweathermap_cities(version):
303 """List all city IDs found in the database"""
304 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
305 return jsonify(result)
308 @app.route('/api/<version>/openweathermap/city/<cityid>')
309 def openweathermap_city(version, cityid):
310 """List all data found for a city"""
311 result = openweathermapdata(cityid=cityid)
312 return jsonify(result)
315 @app.route('/api/<version>/currentairtemperature')
316 def currentair(version):
317 value, timestamp = currentairtemperature(cityid)
318 return jsonify({"value": value, "timestamp": timestamp})
321 @app.route('/api/<version>/currentwatertemperature')
322 def currentwater(version):
323 value, timestamp = currentwatertemperature(mainsensor)
324 return jsonify({"value": value, "timestamp": timestamp})
327 @app.route('/report/<int(fixed_digits=4):year>/<int(fixed_digits=2):month>')
328 def report(year, month):
329 """Report for given year (4 digits) and month (2 digits)
331 paper_size = (29.7 / 2.54, 21. / 2.54) # A4
333 begin = datetime.datetime(year, month, 1)
334 end = first_of_month(begin, 1)
336 water_data = sensordata_to_xy(select_sensordata(mainsensor, 'Wassertemperatur', begin, end))
337 air_data = openweatherdata_to_xy(select_openweatherdata(cityid, begin, end))
339 report_times = [datetime.time(10), datetime.time(15)]
340 report_data = {'Wasser': water_data, 'Luft': air_data}
345 days_datetime.append(d)
346 d = d + datetime.timedelta(1)
348 binary_pdf = io.BytesIO()
349 with PdfPages(binary_pdf) as pdf:
350 title = 'Seepark Obsteig {} {}'.format(MONTH_DE[begin.month-1], begin.year)
353 plt.figure(figsize=paper_size)
355 for label, data in sorted(report_data.items(), reverse=True):
357 lines = plt.plot(x, y, label=label)
358 report_colors.append(lines[0].get_color())
359 plt.xticks(days_datetime, [''] * len(days_datetime))
360 plt.ylabel('Temperatur in °C')
361 plt.axis(xmin=begin, xmax=end)
368 for d in days_datetime:
369 columns.append('{}.'.format(d.day))
371 for label in sorted(report_data.keys(), reverse=True):
372 for t in report_times:
373 rows.append('{:02d}:{:02d} {} °C'.format(t.hour, t.minute, label))
375 for label, data in sorted(report_data.items(), reverse=True):
376 for t in report_times:
379 for d in days_datetime:
380 report_datetime = datetime.datetime.combine(d.date(), t)
384 closest_index = np.argmin(np.abs(x - report_datetime))
385 if abs(x[closest_index] - report_datetime) > datetime.timedelta(hours=1):
388 value = y[closest_index]
389 cell = '{:.1f}'.format(value)
390 row_cells.append(cell)
391 cells.append(row_cells)
392 row_colors = list(ntimes(report_colors + ['w'], len(report_times)))
393 table = plt.table(cellText=cells, colLabels=columns, rowLabels=rows, rowColours=row_colors, loc='bottom')
394 table.scale(xscale=1, yscale=2)
396 plt.subplots_adjust(left=0.15, right=0.97, bottom=0.3) # do not cut row labels
399 pdf_info = pdf.infodict()
400 pdf_info['Title'] = title
401 pdf_info['Author'] = 'Chrisu Jähnl'
402 pdf_info['Subject'] = 'Temperaturen'
403 pdf_info['Keywords'] = 'Seepark Obsteig'
404 pdf_info['CreationDate'] = datetime.datetime.now()
405 pdf_info['ModDate'] = datetime.datetime.today()
407 response = make_response(binary_pdf.getvalue())
408 response.headers['Content-Type'] = 'application/pdf'
409 response.headers['Content-Disposition'] = 'attachment; filename=seepark_{:04d}-{:02d}.pdf'.format(year, month)
415 airvalue, airtime = currentairtemperature(cityid)
416 watervalue, watertime = currentwatertemperature(mainsensor)
417 this_month = first_of_month(datetime.date.today(), 0)
418 last_month = first_of_month(this_month, -1)
420 return render_template(
423 watervalue=watervalue,
427 this_month=this_month,
428 last_month=last_month