7 from collections import defaultdict
12 import matplotlib.pyplot as plt
13 from matplotlib.backends.backend_pdf import PdfPages
15 from flask import Flask, render_template, jsonify, request, abort, Response, make_response
17 from flask_sqlalchemy import SQLAlchemy, inspect
18 from sqlalchemy import func
44 # https://stackoverflow.com/a/37350445
45 def sqlalchemy_model_to_dict(model):
46 return {c.key: getattr(model, c.key)
47 for c in inspect(model).mapper.column_attrs}
50 class JSONEncoder(flask.json.JSONEncoder):
51 def default(self, object):
52 if isinstance(object, datetime.datetime):
53 return object.isoformat()
54 elif isinstance(object, db.Model):
55 return sqlalchemy_model_to_dict(object)
56 return super().default(object)
59 def parse_datetime(date_str):
60 return datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
63 def get_sqlalchemy_database_uri(config):
64 user = config.get('database', 'user')
65 pwd = config.get('database', 'password')
66 host = config.get('database', 'hostname')
67 db = config.get('database', 'database')
68 return 'mysql+mysqldb://{}:{}@{}/{}'.format(user, pwd, host, db)
71 config = configparser.ConfigParser()
72 config.read(os.environ['SEEPARKINI'])
73 apikey = config.get('openweathermap', 'apikey')
74 cityid = config.get('openweathermap', 'cityid')
75 mainsensor = config.get('webapp', 'mainsensor')
78 app.json_encoder = JSONEncoder
79 app.config['SQLALCHEMY_DATABASE_URI'] = get_sqlalchemy_database_uri(config)
80 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
85 class Sensors(db.Model):
86 __tablename__ = 'sensors'
89 class OpenWeatherMap(db.Model):
90 __tablename__ = 'openweathermap'
93 def calc_grouping_resolution(begin, end):
94 """How many data points should be between the timestamps begin and end?"""
95 # copied from munin/master/_bin/munin-cgi-graph.in
96 # except day: 300 -> 600
103 duration = (end - begin).total_seconds()
106 resolution = resolutions['day']
107 elif duration <= 7 * day:
108 resolution = resolutions['week']
109 elif duration <= 31 * day:
110 resolution = resolutions['month']
112 resolution = resolutions['year']
116 def select_sensordata(sensor_id, sensor_type, begin, end):
117 query = Sensors.query
118 if sensor_id is not None:
119 query = query.filter(Sensors.sensor_id == sensor_id)
120 if sensor_type is not None:
121 query = query.filter(Sensors.value_type == sensor_type)
122 if begin is not None:
123 query = query.filter(Sensors.timestamp >= begin)
125 query = query.filter(Sensors.timestamp <= end)
129 def sensordata_to_xy(sensordata):
130 sensordata = list(sensordata)
131 x = np.array([d.timestamp for d in sensordata])
132 y = np.array([d.value for d in sensordata])
136 def select_sensordata_grouped(sensor_id, sensor_type, begin, end):
137 # determine resolution (interval in seconds for data points)
138 resolution = calc_grouping_resolution(begin, end)
140 # Let the database do the grouping. Example in SQL (MySQL):
141 # 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;
142 query = db.session.query(func.to_seconds(Sensors.timestamp).op('div')(resolution).label('g'),
143 func.from_unixtime(func.avg(func.unix_timestamp(Sensors.timestamp))).label('timestamp'),
144 func.avg(Sensors.value).label('value'),
145 Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
146 if sensor_id is not None:
147 query = query.filter(Sensors.sensor_id == sensor_id)
148 if sensor_type is not None:
149 query = query.filter(Sensors.value_type == sensor_type)
150 query = query.filter(Sensors.timestamp >= begin)
151 query = query.filter(Sensors.timestamp <= end)
152 query = query.group_by('g', Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
156 def select_openweatherdata(cityid, begin, end):
157 query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
158 if begin is not None:
159 query = query.filter(OpenWeatherMap.datetime >= begin)
161 query = query.filter(OpenWeatherMap.datetime <= end)
165 def openweatherdata_to_xy(openweatherdata):
166 openweatherdata = list(openweatherdata)
167 x = np.array([d.datetime for d in openweatherdata])
168 y = np.array([d.temp for d in openweatherdata])
172 def select_openweatherdata_grouped(cityid, begin, end):
173 # determine resolution (interval in seconds for data points)
174 resolution = calc_grouping_resolution(begin, end)
176 # Let the database do the grouping. Example in SQL (MySQL):
177 # 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;
178 query = db.session.query(func.to_seconds(OpenWeatherMap.datetime).op('div')(resolution).label('g'),
179 func.from_unixtime(func.avg(func.unix_timestamp(OpenWeatherMap.datetime))).label('datetime'),
180 func.avg(OpenWeatherMap.temp).label('temp'),
181 OpenWeatherMap.cityid)
182 OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
183 query = query.filter(OpenWeatherMap.datetime >= begin)
184 query = query.filter(OpenWeatherMap.datetime <= end)
185 query = query.group_by('g', OpenWeatherMap.cityid)
189 def estimate_swimmer_count(date):
193 def select_swimmerdata(begin, end):
194 def report_times(begin, end):
198 a = datetime.datetime.combine(d.date(), datetime.time(t))
201 d += datetime.timedelta(days=1)
202 SwimmerData = collections.namedtuple('SwimmerData', ['datetime', 'count'])
203 for d in report_times(begin, end):
204 count = estimate_swimmer_count(d)
205 yield SwimmerData(d, count)
208 def swimmerdata_to_xy(swimmerdata):
209 swimmerdata = list(swimmerdata)
210 x = np.array([d.datetime for d in swimmerdata])
211 y = np.array([d.count for d in swimmerdata])
215 def convert_to_c3(result, id, field_x, field_y):
216 c3result = defaultdict(list)
218 c3result[str(getattr(row, id))].append(getattr(row, field_y))
219 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
220 c3result[str(getattr(row, id)) + '_x'].append(dt)
224 def request_arg(key, type, default=None):
225 """Returns the key from the request if available, otherwise the default value.
226 In case type is provided and the key is present, the value is converted by calling type.
227 In other words: Reimplement request.args.get but don't return default value if
228 type raises a ValueError."""
229 if key in request.args:
231 return type(request.args[key])
232 except ValueError as e:
233 abort(Response(str(e), 400))
238 def sensordata(sensor_id=None, sensor_type=None):
239 begin = request_arg('begin', parse_datetime)
240 end = request_arg('end', parse_datetime)
241 mode = request.args.get('mode', 'full')
242 format = request.args.get('format', 'default')
245 result = select_sensordata(sensor_id, sensor_type, begin, end)
246 elif mode == 'consolidated':
247 if begin is None or end is None:
248 abort(Response('begin and end have to be set for mode==consolidated', 400))
249 result = select_sensordata_grouped(sensor_id, sensor_type, begin, end)
251 abort(Response('unknown value for mode', 400))
254 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
258 def openweathermapdata(cityid):
259 begin = request_arg('begin', parse_datetime)
260 end = request_arg('end', parse_datetime)
261 mode = request.args.get('mode', 'full')
262 format = request.args.get('format', 'default')
265 result = select_openweatherdata(cityid, begin, end)
266 elif mode == 'consolidated':
267 if begin is None or end is None:
268 abort(Response('begin and end have to be set for mode==consolidated', 400))
269 result = select_openweatherdata_grouped(cityid, begin, end)
271 abort(Response('unknown value for mode', 400))
274 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
278 def currentairtemperature(cityid):
279 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
280 return result.temp, result.datetime
283 def currentwatertemperature(sensorid):
284 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
285 return result.value, result.timestamp
289 return (date + datetime.timedelta(days=42)).replace(day=1)
292 @app.route('/api/<version>/sensors/')
293 def sensors(version):
294 """List all sensors found in the database"""
295 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
296 return jsonify(result)
299 @app.route('/api/<version>/sensor/id/<sensor_id>')
300 def sensorid(version, sensor_id):
301 """Return all data for a specific sensor
304 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
305 end=<datetime>, optional, format like "2018-05-19T21:07:53"
306 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
307 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
309 result = sensordata(sensor_id=sensor_id)
310 return jsonify(result)
313 @app.route('/api/<version>/sensor/type/<sensor_type>')
314 def sensortype(version, sensor_type):
315 """Return all data for a specific sensor type
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_type=sensor_type)
324 return jsonify(result)
327 @app.route('/api/<version>/openweathermap/cities')
328 def openweathermap_cities(version):
329 """List all city IDs found in the database"""
330 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
331 return jsonify(result)
334 @app.route('/api/<version>/openweathermap/city/<cityid>')
335 def openweathermap_city(version, cityid):
336 """List all data found for a city"""
337 result = openweathermapdata(cityid=cityid)
338 return jsonify(result)
341 @app.route('/api/<version>/currentairtemperature')
342 def currentair(version):
343 value, timestamp = currentairtemperature(cityid)
344 return jsonify({"value": value, "timestamp": timestamp})
347 @app.route('/api/<version>/currentwatertemperature')
348 def currentwater(version):
349 value, timestamp = currentwatertemperature(mainsensor)
350 return jsonify({"value": value, "timestamp": timestamp})
353 @app.route('/report/<int:year>-<int:month>')
354 def report(year, month):
355 paper_size = (29.7 / 2.54, 21. / 2.54) # A4
357 begin = datetime.datetime(year, month, 1)
358 end = add_month(begin)
360 water_data = sensordata_to_xy(select_sensordata(mainsensor, 'Wassertemperatur', begin, end))
361 air_data = openweatherdata_to_xy(select_openweatherdata(cityid, begin, end))
362 swimmer_data = swimmerdata_to_xy(select_swimmerdata(begin, end))
364 report_times = [datetime.time(10), datetime.time(15)]
365 report_data = {'Wasser': water_data, 'Luft': air_data}
370 days_datetime.append(d)
371 d = d + datetime.timedelta(1)
373 binary_pdf = io.BytesIO()
374 with PdfPages(binary_pdf) as pdf:
375 title = 'Seepark Obsteig {} {}'.format(MONTH_DE[begin.month-1], begin.year)
378 plt.figure(figsize=paper_size)
379 for label, data in sorted(report_data.items(), reverse=True):
381 plt.plot(x, y, label=label)
382 plt.xticks(days_datetime, [''] * len(days_datetime))
383 plt.ylabel('Temperatur in °C')
384 plt.axis(xmin=begin, xmax=end)
391 for d in days_datetime:
392 columns.append('{}.'.format(d.day))
394 for label in sorted(report_data.keys(), reverse=True):
395 for t in report_times:
396 rows.append('{:02d}:{:02d} {} °C'.format(t.hour, t.minute, label))
397 for t in report_times:
398 rows.append('{:02d}:{:02d} Badende'.format(t.hour, t.minute))
400 for label, data in sorted(report_data.items(), reverse=True):
401 for t in report_times:
404 for d in days_datetime:
405 report_datetime = datetime.datetime.combine(d.date(), t)
406 closest_index = np.argmin(np.abs(x - report_datetime))
407 if abs(x[closest_index] - report_datetime) > datetime.timedelta(hours=1):
410 value = y[closest_index]
411 cell = '{:.1f}'.format(value)
412 row_cells.append(cell)
413 cells.append(row_cells)
414 for t in report_times:
417 for d in days_datetime:
418 report_datetime = datetime.datetime.combine(d.date(), t)
419 closest_index = np.argmin(np.abs(x - report_datetime))
420 if abs(x[closest_index] - report_datetime) > datetime.timedelta(hours=1):
423 cell = y[closest_index]
424 row_cells.append(cell)
425 cells.append(row_cells)
426 table = plt.table(cellText=cells, colLabels=columns, rowLabels=rows, loc='bottom')
427 table.scale(xscale=1, yscale=2)
429 plt.subplots_adjust(left=0.15, right=0.97, bottom=0.3) # do not cut row labels
432 pdf_info = pdf.infodict()
433 pdf_info['Title'] = title
434 pdf_info['Author'] = 'Chrisu Jähnl'
435 pdf_info['Subject'] = 'Temperaturen'
436 pdf_info['Keywords'] = 'Seepark Obsteig'
437 pdf_info['CreationDate'] = datetime.datetime.now()
438 pdf_info['ModDate'] = datetime.datetime.today()
440 response = make_response(binary_pdf.getvalue())
441 response.headers['Content-Type'] = 'application/pdf'
442 response.headers['Content-Disposition'] = 'attachment; filename=seepark_{:04d}-{:02d}.pdf'.format(year, month)
448 airvalue, airtime = currentairtemperature(cityid)
449 watervalue, watertime = currentwatertemperature(mainsensor)
451 return render_template(
454 watervalue=watervalue,