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
19 from sqlalchemy import func, inspect
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
88 with app.app_context():
92 class Sensors(db.Model):
93 __tablename__ = 'sensors'
96 class OpenWeatherMap(db.Model):
97 __tablename__ = 'openweathermap'
100 def calc_grouping_resolution(begin, end):
101 """How many data points should be between the timestamps begin and end?"""
102 # copied from munin/master/_bin/munin-cgi-graph.in
103 # except day: 300 -> 600
110 duration = (end - begin).total_seconds()
113 resolution = resolutions['day']
114 elif duration <= 7 * day:
115 resolution = resolutions['week']
116 elif duration <= 31 * day:
117 resolution = resolutions['month']
119 resolution = resolutions['year']
123 def select_sensordata(sensor_id, sensor_type, begin, end):
124 query = Sensors.query
125 if sensor_id is not None:
126 query = query.filter(Sensors.sensor_id == sensor_id)
127 if sensor_type is not None:
128 query = query.filter(Sensors.value_type == sensor_type)
129 if begin is not None:
130 query = query.filter(Sensors.timestamp >= begin)
132 query = query.filter(Sensors.timestamp <= end)
136 def sensordata_to_xy(sensordata):
137 sensordata = list(sensordata)
138 x = np.array([d.timestamp for d in sensordata])
139 y = np.array([d.value for d in sensordata])
143 def select_sensordata_grouped(sensor_id, sensor_type, begin, end):
144 # determine resolution (interval in seconds for data points)
145 resolution = calc_grouping_resolution(begin, end)
147 # Let the database do the grouping. Example in SQL (MySQL):
148 # 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;
149 query = db.session.query(func.to_seconds(Sensors.timestamp).op('div')(resolution).label('g'),
150 func.from_unixtime(func.avg(func.unix_timestamp(Sensors.timestamp))).label('timestamp'),
151 func.avg(Sensors.value).label('value'),
152 Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
153 if sensor_id is not None:
154 query = query.filter(Sensors.sensor_id == sensor_id)
155 if sensor_type is not None:
156 query = query.filter(Sensors.value_type == sensor_type)
157 query = query.filter(Sensors.timestamp >= begin)
158 query = query.filter(Sensors.timestamp <= end)
159 query = query.group_by('g', Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
163 def select_openweatherdata(cityid, begin, end):
164 query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
165 if begin is not None:
166 query = query.filter(OpenWeatherMap.datetime >= begin)
168 query = query.filter(OpenWeatherMap.datetime <= end)
172 def openweatherdata_to_xy(openweatherdata):
173 openweatherdata = list(openweatherdata)
174 x = np.array([d.datetime for d in openweatherdata])
175 y = np.array([d.temp for d in openweatherdata])
179 def select_openweatherdata_grouped(cityid, begin, end):
180 # determine resolution (interval in seconds for data points)
181 resolution = calc_grouping_resolution(begin, end)
183 # Let the database do the grouping. Example in SQL (MySQL):
184 # 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;
185 query = db.session.query(func.to_seconds(OpenWeatherMap.datetime).op('div')(resolution).label('g'),
186 func.from_unixtime(func.avg(func.unix_timestamp(OpenWeatherMap.datetime))).label('datetime'),
187 func.avg(OpenWeatherMap.temp).label('temp'),
188 OpenWeatherMap.cityid)
189 OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
190 query = query.filter(OpenWeatherMap.datetime >= begin)
191 query = query.filter(OpenWeatherMap.datetime <= end)
192 query = query.group_by('g', OpenWeatherMap.cityid)
196 def convert_to_c3(result, id, field_x, field_y):
197 c3result = defaultdict(list)
199 c3result[str(getattr(row, id))].append(getattr(row, field_y))
200 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
201 c3result[str(getattr(row, id)) + '_x'].append(dt)
205 def request_arg(key, type, default=None):
206 """Returns the key from the request if available, otherwise the default value.
207 In case type is provided and the key is present, the value is converted by calling type.
208 In other words: Reimplement request.args.get but don't return default value if
209 type raises a ValueError."""
210 if key in request.args:
212 return type(request.args[key])
213 except ValueError as e:
214 abort(Response(str(e), 400))
219 def sensordata(sensor_id=None, sensor_type=None):
220 begin = request_arg('begin', parse_datetime)
221 end = request_arg('end', parse_datetime)
222 mode = request.args.get('mode', 'full')
223 format = request.args.get('format', 'default')
226 result = select_sensordata(sensor_id, sensor_type, begin, end)
227 elif mode == 'consolidated':
228 if begin is None or end is None:
229 abort(Response('begin and end have to be set for mode==consolidated', 400))
230 result = select_sensordata_grouped(sensor_id, sensor_type, begin, end)
232 abort(Response('unknown value for mode', 400))
235 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
239 def openweathermapdata(cityid):
240 begin = request_arg('begin', parse_datetime)
241 end = request_arg('end', parse_datetime)
242 mode = request.args.get('mode', 'full')
243 format = request.args.get('format', 'default')
246 result = select_openweatherdata(cityid, begin, end)
247 elif mode == 'consolidated':
248 if begin is None or end is None:
249 abort(Response('begin and end have to be set for mode==consolidated', 400))
250 result = select_openweatherdata_grouped(cityid, begin, end)
252 abort(Response('unknown value for mode', 400))
255 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
259 def currentairtemperature(cityid):
260 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
261 return result.temp, result.datetime
264 def currentwatertemperature(sensorid):
265 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
266 return result.value, result.timestamp
269 def first_of_month(date, month):
270 date = date.replace(day=1)
274 return (date + datetime.timedelta(days=42)).replace(day=1)
276 return (date - datetime.timedelta(days=1)).replace(day=1)
280 @app.route('/api/<version>/sensors/')
281 def sensors(version):
282 """List all sensors found in the database"""
283 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
284 return jsonify(result)
287 @app.route('/api/<version>/sensor/id/<sensor_id>')
288 def sensorid(version, sensor_id):
289 """Return all data for a specific sensor
293 * ``begin=<datetime>``, optional, format like ``2018-05-19T21:07:53``
294 * ``end=<datetime>``, optional, format like ``2018-05-19T21:07:53``
295 * ``mode=<full|consolidated>``, optional. return all rows (default) or with lower resolution (for charts)
296 * ``format=<default|c3>``, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
298 result = sensordata(sensor_id=sensor_id)
299 return jsonify(result)
302 @app.route('/api/<version>/sensor/type/<sensor_type>')
303 def sensortype(version, sensor_type):
304 """Return all data for a specific sensor type
308 * ``begin=<datetime>``, optional, format like ``2018-05-19T21:07:53``
309 * ``end=<datetime>``, optional, format like ``2018-05-19T21:07:53``
310 * ``mode=<full|consolidated>``, optional. return all rows (default) or with lower resolution (for charts)
311 * ``format=<default|c3>``, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
313 result = sensordata(sensor_type=sensor_type)
314 return jsonify(result)
317 @app.route('/api/<version>/openweathermap/cities')
318 def openweathermap_cities(version):
319 """List all city IDs found in the database"""
320 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
321 return jsonify(result)
324 @app.route('/api/<version>/openweathermap/city/<cityid>')
325 def openweathermap_city(version, cityid):
326 """List all data found for a city"""
327 result = openweathermapdata(cityid=cityid)
328 return jsonify(result)
331 @app.route('/api/<version>/currentairtemperature')
332 def currentair(version):
333 value, timestamp = currentairtemperature(cityid)
334 return jsonify({"value": value, "timestamp": timestamp})
337 @app.route('/api/<version>/currentwatertemperature')
338 def currentwater(version):
339 value, timestamp = currentwatertemperature(mainsensor)
340 return jsonify({"value": value, "timestamp": timestamp})
343 @app.route('/report/<int(fixed_digits=4):year>/<int(fixed_digits=2):month>')
344 def report(year, month):
345 """Report for given year (4 digits) and month (2 digits)
347 paper_size = (29.7 / 2.54, 21. / 2.54) # A4
349 begin = datetime.datetime(year, month, 1)
350 end = first_of_month(begin, 1)
352 water_data = sensordata_to_xy(select_sensordata(mainsensor, 'Wassertemperatur', begin, end))
353 air_data = openweatherdata_to_xy(select_openweatherdata(cityid, begin, end))
355 report_times = [datetime.time(10), datetime.time(15)]
356 report_data = {'Wasser': water_data, 'Luft': air_data}
361 days_datetime.append(d)
362 d = d + datetime.timedelta(1)
364 binary_pdf = io.BytesIO()
365 with PdfPages(binary_pdf) as pdf:
366 title = 'Seepark Obsteig {} {}'.format(MONTH_DE[begin.month-1], begin.year)
369 plt.figure(figsize=paper_size)
371 for label, data in sorted(report_data.items(), reverse=True):
373 lines = plt.plot(x, y, label=label)
374 report_colors.append(lines[0].get_color())
375 plt.xticks(days_datetime, [''] * len(days_datetime))
376 plt.ylabel('Temperatur in °C')
377 plt.axis(xmin=begin, xmax=end)
384 for d in days_datetime:
385 columns.append('{}.'.format(d.day))
387 for label in sorted(report_data.keys(), reverse=True):
388 for t in report_times:
389 rows.append('{:02d}:{:02d} {} °C'.format(t.hour, t.minute, label))
391 for label, data in sorted(report_data.items(), reverse=True):
392 for t in report_times:
395 for d in days_datetime:
396 report_datetime = datetime.datetime.combine(d.date(), t)
397 closest_index = np.argmin(np.abs(x - report_datetime))
398 if abs(x[closest_index] - report_datetime) > datetime.timedelta(hours=1):
401 value = y[closest_index]
402 cell = '{:.1f}'.format(value)
403 row_cells.append(cell)
404 cells.append(row_cells)
405 row_colors = list(ntimes(report_colors + ['w'], len(report_times)))
406 table = plt.table(cellText=cells, colLabels=columns, rowLabels=rows, rowColours=row_colors, loc='bottom')
407 table.scale(xscale=1, yscale=2)
409 plt.subplots_adjust(left=0.15, right=0.97, bottom=0.3) # do not cut row labels
412 pdf_info = pdf.infodict()
413 pdf_info['Title'] = title
414 pdf_info['Author'] = 'Chrisu Jähnl'
415 pdf_info['Subject'] = 'Temperaturen'
416 pdf_info['Keywords'] = 'Seepark Obsteig'
417 pdf_info['CreationDate'] = datetime.datetime.now()
418 pdf_info['ModDate'] = datetime.datetime.today()
420 response = make_response(binary_pdf.getvalue())
421 response.headers['Content-Type'] = 'application/pdf'
422 response.headers['Content-Disposition'] = 'attachment; filename=seepark_{:04d}-{:02d}.pdf'.format(year, month)
428 airvalue, airtime = currentairtemperature(cityid)
429 watervalue, watertime = currentwatertemperature(mainsensor)
430 this_month = first_of_month(datetime.date.today(), 0)
431 last_month = first_of_month(this_month, -1)
433 return render_template(
436 watervalue=watervalue,
440 this_month=this_month,
441 last_month=last_month