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 convert_to_c3(result, id, field_x, field_y):
196 c3result = defaultdict(list)
198 c3result[str(getattr(row, id))].append(getattr(row, field_y))
199 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
200 c3result[str(getattr(row, id)) + '_x'].append(dt)
204 def request_arg(key, type, default=None):
205 """Returns the key from the request if available, otherwise the default value.
206 In case type is provided and the key is present, the value is converted by calling type.
207 In other words: Reimplement request.args.get but don't return default value if
208 type raises a ValueError."""
209 if key in request.args:
211 return type(request.args[key])
212 except ValueError as e:
213 abort(Response(str(e), 400))
218 def sensordata(sensor_id=None, sensor_type=None):
219 begin = request_arg('begin', parse_datetime)
220 end = request_arg('end', parse_datetime)
221 mode = request.args.get('mode', 'full')
222 format = request.args.get('format', 'default')
225 result = select_sensordata(sensor_id, sensor_type, begin, end)
226 elif mode == 'consolidated':
227 if begin is None or end is None:
228 abort(Response('begin and end have to be set for mode==consolidated', 400))
229 result = select_sensordata_grouped(sensor_id, sensor_type, begin, end)
231 abort(Response('unknown value for mode', 400))
234 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
238 def openweathermapdata(cityid):
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_openweatherdata(cityid, 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_openweatherdata_grouped(cityid, begin, end)
251 abort(Response('unknown value for mode', 400))
254 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
258 def currentairtemperature(cityid):
259 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
260 return result.temp, result.datetime
263 def currentwatertemperature(sensorid):
264 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
265 return result.value, result.timestamp
268 def first_of_month(date, month):
269 date = date.replace(day=1)
273 return (date + datetime.timedelta(days=42)).replace(day=1)
275 return (date - datetime.timedelta(days=1)).replace(day=1)
279 @app.route('/api/<version>/sensors/')
280 def sensors(version):
281 """List all sensors found in the database"""
282 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
283 return jsonify(result)
286 @app.route('/api/<version>/sensor/id/<sensor_id>')
287 def sensorid(version, sensor_id):
288 """Return all data for a specific sensor
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_id=sensor_id)
298 return jsonify(result)
301 @app.route('/api/<version>/sensor/type/<sensor_type>')
302 def sensortype(version, sensor_type):
303 """Return all data for a specific sensor type
307 * ``begin=<datetime>``, optional, format like ``2018-05-19T21:07:53``
308 * ``end=<datetime>``, optional, format like ``2018-05-19T21:07:53``
309 * ``mode=<full|consolidated>``, optional. return all rows (default) or with lower resolution (for charts)
310 * ``format=<default|c3>``, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
312 result = sensordata(sensor_type=sensor_type)
313 return jsonify(result)
316 @app.route('/api/<version>/openweathermap/cities')
317 def openweathermap_cities(version):
318 """List all city IDs found in the database"""
319 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
320 return jsonify(result)
323 @app.route('/api/<version>/openweathermap/city/<cityid>')
324 def openweathermap_city(version, cityid):
325 """List all data found for a city"""
326 result = openweathermapdata(cityid=cityid)
327 return jsonify(result)
330 @app.route('/api/<version>/currentairtemperature')
331 def currentair(version):
332 value, timestamp = currentairtemperature(cityid)
333 return jsonify({"value": value, "timestamp": timestamp})
336 @app.route('/api/<version>/currentwatertemperature')
337 def currentwater(version):
338 value, timestamp = currentwatertemperature(mainsensor)
339 return jsonify({"value": value, "timestamp": timestamp})
342 @app.route('/report/<int(fixed_digits=4):year>/<int(fixed_digits=2):month>')
343 def report(year, month):
344 """Report for given year (4 digits) and month (2 digits)
346 paper_size = (29.7 / 2.54, 21. / 2.54) # A4
348 begin = datetime.datetime(year, month, 1)
349 end = first_of_month(begin, 1)
351 water_data = sensordata_to_xy(select_sensordata(mainsensor, 'Wassertemperatur', begin, end))
352 air_data = openweatherdata_to_xy(select_openweatherdata(cityid, begin, end))
354 report_times = [datetime.time(10), datetime.time(15)]
355 report_data = {'Wasser': water_data, 'Luft': air_data}
360 days_datetime.append(d)
361 d = d + datetime.timedelta(1)
363 binary_pdf = io.BytesIO()
364 with PdfPages(binary_pdf) as pdf:
365 title = 'Seepark Obsteig {} {}'.format(MONTH_DE[begin.month-1], begin.year)
368 plt.figure(figsize=paper_size)
370 for label, data in sorted(report_data.items(), reverse=True):
372 lines = plt.plot(x, y, label=label)
373 report_colors.append(lines[0].get_color())
374 plt.xticks(days_datetime, [''] * len(days_datetime))
375 plt.ylabel('Temperatur in °C')
376 plt.axis(xmin=begin, xmax=end)
383 for d in days_datetime:
384 columns.append('{}.'.format(d.day))
386 for label in sorted(report_data.keys(), reverse=True):
387 for t in report_times:
388 rows.append('{:02d}:{:02d} {} °C'.format(t.hour, t.minute, label))
390 for label, data in sorted(report_data.items(), reverse=True):
391 for t in report_times:
394 for d in days_datetime:
395 report_datetime = datetime.datetime.combine(d.date(), t)
396 closest_index = np.argmin(np.abs(x - report_datetime))
397 if abs(x[closest_index] - report_datetime) > datetime.timedelta(hours=1):
400 value = y[closest_index]
401 cell = '{:.1f}'.format(value)
402 row_cells.append(cell)
403 cells.append(row_cells)
404 row_colors = list(ntimes(report_colors + ['w'], len(report_times)))
405 table = plt.table(cellText=cells, colLabels=columns, rowLabels=rows, rowColours=row_colors, loc='bottom')
406 table.scale(xscale=1, yscale=2)
408 plt.subplots_adjust(left=0.15, right=0.97, bottom=0.3) # do not cut row labels
411 pdf_info = pdf.infodict()
412 pdf_info['Title'] = title
413 pdf_info['Author'] = 'Chrisu Jähnl'
414 pdf_info['Subject'] = 'Temperaturen'
415 pdf_info['Keywords'] = 'Seepark Obsteig'
416 pdf_info['CreationDate'] = datetime.datetime.now()
417 pdf_info['ModDate'] = datetime.datetime.today()
419 response = make_response(binary_pdf.getvalue())
420 response.headers['Content-Type'] = 'application/pdf'
421 response.headers['Content-Disposition'] = 'attachment; filename=seepark_{:04d}-{:02d}.pdf'.format(year, month)
427 airvalue, airtime = currentairtemperature(cityid)
428 watervalue, watertime = currentwatertemperature(mainsensor)
429 this_month = first_of_month(datetime.date.today(), 0)
430 last_month = first_of_month(this_month, -1)
432 return render_template(
435 watervalue=watervalue,
439 this_month=this_month,
440 last_month=last_month