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
295 return (date + datetime.timedelta(days=42)).replace(day=1)
298 @app.route('/api/<version>/sensors/')
299 def sensors(version):
300 """List all sensors found in the database"""
301 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
302 return jsonify(result)
305 @app.route('/api/<version>/sensor/id/<sensor_id>')
306 def sensorid(version, sensor_id):
307 """Return all data for a specific sensor
310 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
311 end=<datetime>, optional, format like "2018-05-19T21:07:53"
312 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
313 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
315 result = sensordata(sensor_id=sensor_id)
316 return jsonify(result)
319 @app.route('/api/<version>/sensor/type/<sensor_type>')
320 def sensortype(version, sensor_type):
321 """Return all data for a specific sensor type
324 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
325 end=<datetime>, optional, format like "2018-05-19T21:07:53"
326 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
327 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
329 result = sensordata(sensor_type=sensor_type)
330 return jsonify(result)
333 @app.route('/api/<version>/openweathermap/cities')
334 def openweathermap_cities(version):
335 """List all city IDs found in the database"""
336 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
337 return jsonify(result)
340 @app.route('/api/<version>/openweathermap/city/<cityid>')
341 def openweathermap_city(version, cityid):
342 """List all data found for a city"""
343 result = openweathermapdata(cityid=cityid)
344 return jsonify(result)
347 @app.route('/api/<version>/currentairtemperature')
348 def currentair(version):
349 value, timestamp = currentairtemperature(cityid)
350 return jsonify({"value": value, "timestamp": timestamp})
353 @app.route('/api/<version>/currentwatertemperature')
354 def currentwater(version):
355 value, timestamp = currentwatertemperature(mainsensor)
356 return jsonify({"value": value, "timestamp": timestamp})
359 @app.route('/report/<int:year>-<int:month>')
360 def report(year, month):
361 paper_size = (29.7 / 2.54, 21. / 2.54) # A4
363 begin = datetime.datetime(year, month, 1)
364 end = add_month(begin)
366 water_data = sensordata_to_xy(select_sensordata(mainsensor, 'Wassertemperatur', begin, end))
367 air_data = openweatherdata_to_xy(select_openweatherdata(cityid, begin, end))
368 swimmer_data = swimmerdata_to_xy(select_swimmerdata(begin, end))
370 report_times = [datetime.time(10), datetime.time(15)]
371 report_data = {'Wasser': water_data, 'Luft': air_data}
376 days_datetime.append(d)
377 d = d + datetime.timedelta(1)
379 binary_pdf = io.BytesIO()
380 with PdfPages(binary_pdf) as pdf:
381 title = 'Seepark Obsteig {} {}'.format(MONTH_DE[begin.month-1], begin.year)
384 plt.figure(figsize=paper_size)
386 for label, data in sorted(report_data.items(), reverse=True):
388 lines = plt.plot(x, y, label=label)
389 report_colors.append(lines[0].get_color())
390 plt.xticks(days_datetime, [''] * len(days_datetime))
391 plt.ylabel('Temperatur in °C')
392 plt.axis(xmin=begin, xmax=end)
399 for d in days_datetime:
400 columns.append('{}.'.format(d.day))
402 for label in sorted(report_data.keys(), reverse=True):
403 for t in report_times:
404 rows.append('{:02d}:{:02d} {} °C'.format(t.hour, t.minute, label))
405 for t in report_times:
406 rows.append('{:02d}:{:02d} Badende'.format(t.hour, t.minute))
408 for label, data in sorted(report_data.items(), reverse=True):
409 for t in report_times:
412 for d in days_datetime:
413 report_datetime = datetime.datetime.combine(d.date(), t)
414 closest_index = np.argmin(np.abs(x - report_datetime))
415 if abs(x[closest_index] - report_datetime) > datetime.timedelta(hours=1):
418 value = y[closest_index]
419 cell = '{:.1f}'.format(value)
420 row_cells.append(cell)
421 cells.append(row_cells)
422 for t in report_times:
425 for d in days_datetime:
426 report_datetime = datetime.datetime.combine(d.date(), t)
427 closest_index = np.argmin(np.abs(x - report_datetime))
428 if abs(x[closest_index] - report_datetime) > datetime.timedelta(hours=1):
431 cell = y[closest_index]
432 row_cells.append(cell)
433 cells.append(row_cells)
434 row_colors = list(ntimes(report_colors + ['w'], len(report_times)))
435 table = plt.table(cellText=cells, colLabels=columns, rowLabels=rows, rowColours=row_colors, loc='bottom')
436 table.scale(xscale=1, yscale=2)
438 plt.subplots_adjust(left=0.15, right=0.97, bottom=0.3) # do not cut row labels
441 pdf_info = pdf.infodict()
442 pdf_info['Title'] = title
443 pdf_info['Author'] = 'Chrisu Jähnl'
444 pdf_info['Subject'] = 'Temperaturen'
445 pdf_info['Keywords'] = 'Seepark Obsteig'
446 pdf_info['CreationDate'] = datetime.datetime.now()
447 pdf_info['ModDate'] = datetime.datetime.today()
449 response = make_response(binary_pdf.getvalue())
450 response.headers['Content-Type'] = 'application/pdf'
451 response.headers['Content-Disposition'] = 'attachment; filename=seepark_{:04d}-{:02d}.pdf'.format(year, month)
457 airvalue, airtime = currentairtemperature(cityid)
458 watervalue, watertime = currentwatertemperature(mainsensor)
460 return render_template(
463 watervalue=watervalue,