8 from collections import defaultdict
13 import matplotlib.pyplot as plt
14 from matplotlib.backends.backend_pdf import PdfPages
16 from flask import Flask, render_template, jsonify, request, abort, Response, make_response
18 from flask_sqlalchemy import SQLAlchemy, inspect
19 from sqlalchemy import func
45 # https://stackoverflow.com/a/37350445
46 def sqlalchemy_model_to_dict(model):
47 return {c.key: getattr(model, c.key)
48 for c in inspect(model).mapper.column_attrs}
51 class JSONEncoder(flask.json.JSONEncoder):
52 def default(self, object):
53 if isinstance(object, datetime.datetime):
54 return object.isoformat()
55 elif isinstance(object, db.Model):
56 return sqlalchemy_model_to_dict(object)
57 return super().default(object)
60 def parse_datetime(date_str):
61 return datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
66 yield from itertools.repeat(v, n)
69 def get_sqlalchemy_database_uri(config):
70 user = config.get('database', 'user')
71 pwd = config.get('database', 'password')
72 host = config.get('database', 'hostname')
73 db = config.get('database', 'database')
74 return 'mysql+mysqldb://{}:{}@{}/{}'.format(user, pwd, host, db)
77 config = configparser.ConfigParser()
78 config.read(os.environ['SEEPARKINI'])
79 apikey = config.get('openweathermap', 'apikey')
80 cityid = config.get('openweathermap', 'cityid')
81 mainsensor = config.get('webapp', 'mainsensor')
84 app.json_encoder = JSONEncoder
85 app.config['SQLALCHEMY_DATABASE_URI'] = get_sqlalchemy_database_uri(config)
86 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
91 class Sensors(db.Model):
92 __tablename__ = 'sensors'
95 class OpenWeatherMap(db.Model):
96 __tablename__ = 'openweathermap'
99 def calc_grouping_resolution(begin, end):
100 """How many data points should be between the timestamps begin and end?"""
101 # copied from munin/master/_bin/munin-cgi-graph.in
102 # except day: 300 -> 600
109 duration = (end - begin).total_seconds()
112 resolution = resolutions['day']
113 elif duration <= 7 * day:
114 resolution = resolutions['week']
115 elif duration <= 31 * day:
116 resolution = resolutions['month']
118 resolution = resolutions['year']
122 def select_sensordata(sensor_id, sensor_type, begin, end):
123 query = Sensors.query
124 if sensor_id is not None:
125 query = query.filter(Sensors.sensor_id == sensor_id)
126 if sensor_type is not None:
127 query = query.filter(Sensors.value_type == sensor_type)
128 if begin is not None:
129 query = query.filter(Sensors.timestamp >= begin)
131 query = query.filter(Sensors.timestamp <= end)
135 def sensordata_to_xy(sensordata):
136 sensordata = list(sensordata)
137 x = np.array([d.timestamp for d in sensordata])
138 y = np.array([d.value for d in sensordata])
142 def select_sensordata_grouped(sensor_id, sensor_type, begin, end):
143 # determine resolution (interval in seconds for data points)
144 resolution = calc_grouping_resolution(begin, end)
146 # Let the database do the grouping. Example in SQL (MySQL):
147 # 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;
148 query = db.session.query(func.to_seconds(Sensors.timestamp).op('div')(resolution).label('g'),
149 func.from_unixtime(func.avg(func.unix_timestamp(Sensors.timestamp))).label('timestamp'),
150 func.avg(Sensors.value).label('value'),
151 Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
152 if sensor_id is not None:
153 query = query.filter(Sensors.sensor_id == sensor_id)
154 if sensor_type is not None:
155 query = query.filter(Sensors.value_type == sensor_type)
156 query = query.filter(Sensors.timestamp >= begin)
157 query = query.filter(Sensors.timestamp <= end)
158 query = query.group_by('g', Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
162 def select_openweatherdata(cityid, begin, end):
163 query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
164 if begin is not None:
165 query = query.filter(OpenWeatherMap.datetime >= begin)
167 query = query.filter(OpenWeatherMap.datetime <= end)
171 def openweatherdata_to_xy(openweatherdata):
172 openweatherdata = list(openweatherdata)
173 x = np.array([d.datetime for d in openweatherdata])
174 y = np.array([d.temp for d in openweatherdata])
178 def select_openweatherdata_grouped(cityid, begin, end):
179 # determine resolution (interval in seconds for data points)
180 resolution = calc_grouping_resolution(begin, end)
182 # Let the database do the grouping. Example in SQL (MySQL):
183 # 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;
184 query = db.session.query(func.to_seconds(OpenWeatherMap.datetime).op('div')(resolution).label('g'),
185 func.from_unixtime(func.avg(func.unix_timestamp(OpenWeatherMap.datetime))).label('datetime'),
186 func.avg(OpenWeatherMap.temp).label('temp'),
187 OpenWeatherMap.cityid)
188 OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
189 query = query.filter(OpenWeatherMap.datetime >= begin)
190 query = query.filter(OpenWeatherMap.datetime <= end)
191 query = query.group_by('g', OpenWeatherMap.cityid)
195 def estimate_swimmer_count(date):
199 def select_swimmerdata(begin, end):
200 def report_times(begin, end):
204 a = datetime.datetime.combine(d.date(), datetime.time(t))
207 d += datetime.timedelta(days=1)
208 SwimmerData = collections.namedtuple('SwimmerData', ['datetime', 'count'])
209 for d in report_times(begin, end):
210 count = estimate_swimmer_count(d)
211 yield SwimmerData(d, count)
214 def swimmerdata_to_xy(swimmerdata):
215 swimmerdata = list(swimmerdata)
216 x = np.array([d.datetime for d in swimmerdata])
217 y = np.array([d.count for d in swimmerdata])
221 def convert_to_c3(result, id, field_x, field_y):
222 c3result = defaultdict(list)
224 c3result[str(getattr(row, id))].append(getattr(row, field_y))
225 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
226 c3result[str(getattr(row, id)) + '_x'].append(dt)
230 def request_arg(key, type, default=None):
231 """Returns the key from the request if available, otherwise the default value.
232 In case type is provided and the key is present, the value is converted by calling type.
233 In other words: Reimplement request.args.get but don't return default value if
234 type raises a ValueError."""
235 if key in request.args:
237 return type(request.args[key])
238 except ValueError as e:
239 abort(Response(str(e), 400))
244 def sensordata(sensor_id=None, sensor_type=None):
245 begin = request_arg('begin', parse_datetime)
246 end = request_arg('end', parse_datetime)
247 mode = request.args.get('mode', 'full')
248 format = request.args.get('format', 'default')
251 result = select_sensordata(sensor_id, sensor_type, begin, end)
252 elif mode == 'consolidated':
253 if begin is None or end is None:
254 abort(Response('begin and end have to be set for mode==consolidated', 400))
255 result = select_sensordata_grouped(sensor_id, sensor_type, begin, end)
257 abort(Response('unknown value for mode', 400))
260 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
264 def openweathermapdata(cityid):
265 begin = request_arg('begin', parse_datetime)
266 end = request_arg('end', parse_datetime)
267 mode = request.args.get('mode', 'full')
268 format = request.args.get('format', 'default')
271 result = select_openweatherdata(cityid, begin, end)
272 elif mode == 'consolidated':
273 if begin is None or end is None:
274 abort(Response('begin and end have to be set for mode==consolidated', 400))
275 result = select_openweatherdata_grouped(cityid, begin, end)
277 abort(Response('unknown value for mode', 400))
280 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
284 def currentairtemperature(cityid):
285 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
286 return result.temp, result.datetime
289 def currentwatertemperature(sensorid):
290 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
291 return result.value, result.timestamp
294 def first_of_month(date, month):
295 date = date.replace(day=1)
299 return (date + datetime.timedelta(days=42)).replace(day=1)
301 return (date - datetime.timedelta(days=1)).replace(day=1)
305 @app.route('/api/<version>/sensors/')
306 def sensors(version):
307 """List all sensors found in the database"""
308 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
309 return jsonify(result)
312 @app.route('/api/<version>/sensor/id/<sensor_id>')
313 def sensorid(version, sensor_id):
314 """Return all data for a specific sensor
318 * ``begin=<datetime>``, optional, format like ``2018-05-19T21:07:53``
319 * ``end=<datetime>``, optional, format like ``2018-05-19T21:07:53``
320 * ``mode=<full|consolidated>``, optional. return all rows (default) or with lower resolution (for charts)
321 * ``format=<default|c3>``, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
323 result = sensordata(sensor_id=sensor_id)
324 return jsonify(result)
327 @app.route('/api/<version>/sensor/type/<sensor_type>')
328 def sensortype(version, sensor_type):
329 """Return all data for a specific sensor type
333 * ``begin=<datetime>``, optional, format like ``2018-05-19T21:07:53``
334 * ``end=<datetime>``, optional, format like ``2018-05-19T21:07:53``
335 * ``mode=<full|consolidated>``, optional. return all rows (default) or with lower resolution (for charts)
336 * ``format=<default|c3>``, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
338 result = sensordata(sensor_type=sensor_type)
339 return jsonify(result)
342 @app.route('/api/<version>/openweathermap/cities')
343 def openweathermap_cities(version):
344 """List all city IDs found in the database"""
345 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
346 return jsonify(result)
349 @app.route('/api/<version>/openweathermap/city/<cityid>')
350 def openweathermap_city(version, cityid):
351 """List all data found for a city"""
352 result = openweathermapdata(cityid=cityid)
353 return jsonify(result)
356 @app.route('/api/<version>/currentairtemperature')
357 def currentair(version):
358 value, timestamp = currentairtemperature(cityid)
359 return jsonify({"value": value, "timestamp": timestamp})
362 @app.route('/api/<version>/currentwatertemperature')
363 def currentwater(version):
364 value, timestamp = currentwatertemperature(mainsensor)
365 return jsonify({"value": value, "timestamp": timestamp})
368 @app.route('/report/<int(fixed_digits=4):year>/<int(fixed_digits=2):month>')
369 def report(year, month):
370 """Report for given year (4 digits) and month (2 digits)
372 paper_size = (29.7 / 2.54, 21. / 2.54) # A4
374 begin = datetime.datetime(year, month, 1)
375 end = first_of_month(begin, 1)
377 water_data = sensordata_to_xy(select_sensordata(mainsensor, 'Wassertemperatur', begin, end))
378 air_data = openweatherdata_to_xy(select_openweatherdata(cityid, begin, end))
379 swimmer_data = swimmerdata_to_xy(select_swimmerdata(begin, end))
381 report_times = [datetime.time(10), datetime.time(15)]
382 report_data = {'Wasser': water_data, 'Luft': air_data}
387 days_datetime.append(d)
388 d = d + datetime.timedelta(1)
390 binary_pdf = io.BytesIO()
391 with PdfPages(binary_pdf) as pdf:
392 title = 'Seepark Obsteig {} {}'.format(MONTH_DE[begin.month-1], begin.year)
395 plt.figure(figsize=paper_size)
397 for label, data in sorted(report_data.items(), reverse=True):
399 lines = plt.plot(x, y, label=label)
400 report_colors.append(lines[0].get_color())
401 plt.xticks(days_datetime, [''] * len(days_datetime))
402 plt.ylabel('Temperatur in °C')
403 plt.axis(xmin=begin, xmax=end)
410 for d in days_datetime:
411 columns.append('{}.'.format(d.day))
413 for label in sorted(report_data.keys(), reverse=True):
414 for t in report_times:
415 rows.append('{:02d}:{:02d} {} °C'.format(t.hour, t.minute, label))
416 for t in report_times:
417 rows.append('{:02d}:{:02d} Badende'.format(t.hour, t.minute))
419 for label, data in sorted(report_data.items(), reverse=True):
420 for t in report_times:
423 for d in days_datetime:
424 report_datetime = datetime.datetime.combine(d.date(), t)
425 closest_index = np.argmin(np.abs(x - report_datetime))
426 if abs(x[closest_index] - report_datetime) > datetime.timedelta(hours=1):
429 value = y[closest_index]
430 cell = '{:.1f}'.format(value)
431 row_cells.append(cell)
432 cells.append(row_cells)
433 for t in report_times:
436 for d in days_datetime:
437 report_datetime = datetime.datetime.combine(d.date(), t)
438 closest_index = np.argmin(np.abs(x - report_datetime))
439 if abs(x[closest_index] - report_datetime) > datetime.timedelta(hours=1):
442 cell = y[closest_index]
443 row_cells.append(cell)
444 cells.append(row_cells)
445 row_colors = list(ntimes(report_colors + ['w'], len(report_times)))
446 table = plt.table(cellText=cells, colLabels=columns, rowLabels=rows, rowColours=row_colors, loc='bottom')
447 table.scale(xscale=1, yscale=2)
449 plt.subplots_adjust(left=0.15, right=0.97, bottom=0.3) # do not cut row labels
452 pdf_info = pdf.infodict()
453 pdf_info['Title'] = title
454 pdf_info['Author'] = 'Chrisu Jähnl'
455 pdf_info['Subject'] = 'Temperaturen'
456 pdf_info['Keywords'] = 'Seepark Obsteig'
457 pdf_info['CreationDate'] = datetime.datetime.now()
458 pdf_info['ModDate'] = datetime.datetime.today()
460 response = make_response(binary_pdf.getvalue())
461 response.headers['Content-Type'] = 'application/pdf'
462 response.headers['Content-Disposition'] = 'attachment; filename=seepark_{:04d}-{:02d}.pdf'.format(year, month)
468 airvalue, airtime = currentairtemperature(cityid)
469 watervalue, watertime = currentwatertemperature(mainsensor)
470 this_month = first_of_month(datetime.date.today(), 0)
471 last_month = first_of_month(this_month, -1)
473 return render_template(
476 watervalue=watervalue,
480 this_month=this_month,
481 last_month=last_month