6 from collections import defaultdict
11 import matplotlib.pyplot as plt
12 from matplotlib.backends.backend_pdf import PdfPages
14 from flask import Flask, render_template, jsonify, request, abort, Response, make_response
16 from flask_sqlalchemy import SQLAlchemy, inspect
17 from sqlalchemy import func
43 # https://stackoverflow.com/a/37350445
44 def sqlalchemy_model_to_dict(model):
45 return {c.key: getattr(model, c.key)
46 for c in inspect(model).mapper.column_attrs}
49 class JSONEncoder(flask.json.JSONEncoder):
50 def default(self, object):
51 if isinstance(object, datetime.datetime):
52 return object.isoformat()
53 elif isinstance(object, db.Model):
54 return sqlalchemy_model_to_dict(object)
55 return super().default(object)
58 def parse_datetime(date_str):
59 return datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
62 def get_sqlalchemy_database_uri(config):
63 user = config.get('database', 'user')
64 pwd = config.get('database', 'password')
65 host = config.get('database', 'hostname')
66 db = config.get('database', 'database')
67 return 'mysql+mysqldb://{}:{}@{}/{}'.format(user, pwd, host, db)
70 config = configparser.ConfigParser()
71 config.read(os.environ['SEEPARKINI'])
72 apikey = config.get('openweathermap', 'apikey')
73 cityid = config.get('openweathermap', 'cityid')
74 mainsensor = config.get('webapp', 'mainsensor')
77 app.json_encoder = JSONEncoder
78 app.config['SQLALCHEMY_DATABASE_URI'] = get_sqlalchemy_database_uri(config)
79 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
84 class Sensors(db.Model):
85 __tablename__ = 'sensors'
88 class OpenWeatherMap(db.Model):
89 __tablename__ = 'openweathermap'
92 def calc_grouping_resolution(begin, end):
93 """How many data points should be between the timestamps begin and end?"""
94 # copied from munin/master/_bin/munin-cgi-graph.in
95 # except day: 300 -> 600
102 duration = (end - begin).total_seconds()
105 resolution = resolutions['day']
106 elif duration <= 7 * day:
107 resolution = resolutions['week']
108 elif duration <= 31 * day:
109 resolution = resolutions['month']
111 resolution = resolutions['year']
115 def select_sensordata(sensor_id, sensor_type, begin, end):
116 query = Sensors.query
117 if sensor_id is not None:
118 query = query.filter(Sensors.sensor_id == sensor_id)
119 if sensor_type is not None:
120 query = query.filter(Sensors.value_type == sensor_type)
121 if begin is not None:
122 query = query.filter(Sensors.timestamp >= begin)
124 query = query.filter(Sensors.timestamp <= end)
128 def sensordata_to_xy(sensordata):
129 sensordata = list(sensordata)
130 x = np.array([d.timestamp for d in sensordata])
131 y = np.array([d.value for d in sensordata])
135 def select_sensordata_grouped(sensor_id, sensor_type, begin, end):
136 # determine resolution (interval in seconds for data points)
137 resolution = calc_grouping_resolution(begin, end)
139 # Let the database do the grouping. Example in SQL (MySQL):
140 # 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;
141 query = db.session.query(func.to_seconds(Sensors.timestamp).op('div')(resolution).label('g'),
142 func.from_unixtime(func.avg(func.unix_timestamp(Sensors.timestamp))).label('timestamp'),
143 func.avg(Sensors.value).label('value'),
144 Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
145 if sensor_id is not None:
146 query = query.filter(Sensors.sensor_id == sensor_id)
147 if sensor_type is not None:
148 query = query.filter(Sensors.value_type == sensor_type)
149 query = query.filter(Sensors.timestamp >= begin)
150 query = query.filter(Sensors.timestamp <= end)
151 query = query.group_by('g', Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
155 def select_openweatherdata(cityid, begin, end):
156 query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
157 if begin is not None:
158 query = query.filter(OpenWeatherMap.datetime >= begin)
160 query = query.filter(OpenWeatherMap.datetime <= end)
164 def openweatherdata_to_xy(openweatherdata):
165 openweatherdata = list(openweatherdata)
166 x = np.array([d.datetime for d in openweatherdata])
167 y = np.array([d.temp for d in openweatherdata])
171 def select_openweatherdata_grouped(cityid, begin, end):
172 # determine resolution (interval in seconds for data points)
173 resolution = calc_grouping_resolution(begin, end)
175 # Let the database do the grouping. Example in SQL (MySQL):
176 # 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;
177 query = db.session.query(func.to_seconds(OpenWeatherMap.datetime).op('div')(resolution).label('g'),
178 func.from_unixtime(func.avg(func.unix_timestamp(OpenWeatherMap.datetime))).label('datetime'),
179 func.avg(OpenWeatherMap.temp).label('temp'),
180 OpenWeatherMap.cityid)
181 OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
182 query = query.filter(OpenWeatherMap.datetime >= begin)
183 query = query.filter(OpenWeatherMap.datetime <= end)
184 query = query.group_by('g', OpenWeatherMap.cityid)
188 def convert_to_c3(result, id, field_x, field_y):
189 c3result = defaultdict(list)
191 c3result[str(getattr(row, id))].append(getattr(row, field_y))
192 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
193 c3result[str(getattr(row, id)) + '_x'].append(dt)
197 def request_arg(key, type, default=None):
198 """Returns the key from the request if available, otherwise the default value.
199 In case type is provided and the key is present, the value is converted by calling type.
200 In other words: Reimplement request.args.get but don't return default value if
201 type raises a ValueError."""
202 if key in request.args:
204 return type(request.args[key])
205 except ValueError as e:
206 abort(Response(str(e), 400))
211 def sensordata(sensor_id=None, sensor_type=None):
212 begin = request_arg('begin', parse_datetime)
213 end = request_arg('end', parse_datetime)
214 mode = request.args.get('mode', 'full')
215 format = request.args.get('format', 'default')
218 result = select_sensordata(sensor_id, sensor_type, begin, end)
219 elif mode == 'consolidated':
220 if begin is None or end is None:
221 abort(Response('begin and end have to be set for mode==consolidated', 400))
222 result = select_sensordata_grouped(sensor_id, sensor_type, begin, end)
224 abort(Response('unknown value for mode', 400))
227 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
231 def openweathermapdata(cityid):
232 begin = request_arg('begin', parse_datetime)
233 end = request_arg('end', parse_datetime)
234 mode = request.args.get('mode', 'full')
235 format = request.args.get('format', 'default')
238 result = select_openweatherdata(cityid, begin, end)
239 elif mode == 'consolidated':
240 if begin is None or end is None:
241 abort(Response('begin and end have to be set for mode==consolidated', 400))
242 result = select_openweatherdata_grouped(cityid, begin, end)
244 abort(Response('unknown value for mode', 400))
247 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
251 def currentairtemperature(cityid):
252 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
253 return result.temp, result.datetime
256 def currentwatertemperature(sensorid):
257 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
258 return result.value, result.timestamp
262 return (date + datetime.timedelta(days=42)).replace(day=1)
265 @app.route('/api/<version>/sensors/')
266 def sensors(version):
267 """List all sensors found in the database"""
268 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
269 return jsonify(result)
272 @app.route('/api/<version>/sensor/id/<sensor_id>')
273 def sensorid(version, sensor_id):
274 """Return all data for a specific sensor
277 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
278 end=<datetime>, optional, format like "2018-05-19T21:07:53"
279 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
280 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
282 result = sensordata(sensor_id=sensor_id)
283 return jsonify(result)
286 @app.route('/api/<version>/sensor/type/<sensor_type>')
287 def sensortype(version, sensor_type):
288 """Return all data for a specific sensor type
291 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
292 end=<datetime>, optional, format like "2018-05-19T21:07:53"
293 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
294 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
296 result = sensordata(sensor_type=sensor_type)
297 return jsonify(result)
300 @app.route('/api/<version>/openweathermap/cities')
301 def openweathermap_cities(version):
302 """List all city IDs found in the database"""
303 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
304 return jsonify(result)
307 @app.route('/api/<version>/openweathermap/city/<cityid>')
308 def openweathermap_city(version, cityid):
309 """List all data found for a city"""
310 result = openweathermapdata(cityid=cityid)
311 return jsonify(result)
314 @app.route('/api/<version>/currentairtemperature')
315 def currentair(version):
316 value, timestamp = currentairtemperature(cityid)
317 return jsonify({"value": value, "timestamp": timestamp})
320 @app.route('/api/<version>/currentwatertemperature')
321 def currentwater(version):
322 value, timestamp = currentwatertemperature(mainsensor)
323 return jsonify({"value": value, "timestamp": timestamp})
326 @app.route('/report/<int:year>-<int:month>')
327 def report(year, month):
328 paper_size = (29.7 / 2.54, 21. / 2.54) # A4
330 begin = datetime.datetime(year, month, 1)
331 end = add_month(begin)
333 water_data = sensordata_to_xy(select_sensordata(mainsensor, 'Wassertemperatur', begin, end))
334 air_data = openweatherdata_to_xy(select_openweatherdata(cityid, begin, end))
336 report_times = [datetime.time(10), datetime.time(15)]
337 report_data = {'Wasser': water_data, 'Luft': air_data}
342 days_datetime.append(d)
343 d = d + datetime.timedelta(1)
345 binary_pdf = io.BytesIO()
346 with PdfPages(binary_pdf) as pdf:
347 title = 'Seepark Obsteig {} {}'.format(MONTH_DE[begin.month-1], begin.year)
350 plt.figure(figsize=paper_size)
351 for label, data in sorted(report_data.items(), reverse=True):
353 plt.plot(x, y, label=label)
354 plt.xticks(days_datetime, [''] * len(days_datetime))
355 plt.ylabel('Temperatur in °C')
356 plt.axis(xmin=begin, xmax=end)
363 for d in days_datetime:
364 columns.append('{}.'.format(d.day))
366 for label in sorted(report_data.keys(), reverse=True):
367 for t in report_times:
368 rows.append('{:02d}:{:02d} {} °C'.format(t.hour, t.minute, label))
370 for label, data in sorted(report_data.items(), reverse=True):
371 for t in report_times:
374 for d in days_datetime:
375 report_datetime = datetime.datetime.combine(d.date(), t)
376 closest_index = np.argmin(np.abs(x - report_datetime))
377 if abs(x[closest_index] - report_datetime) > datetime.timedelta(hours=1):
380 value = y[closest_index]
381 cell = '{:.1f}'.format(value)
382 row_cells.append(cell)
383 cells.append(row_cells)
384 table = plt.table(cellText=cells, colLabels=columns, rowLabels=rows, loc='bottom')
385 table.scale(xscale=1, yscale=2)
387 plt.subplots_adjust(left=0.15, right=0.97, bottom=0.3) # do not cut row labels
390 pdf_info = pdf.infodict()
391 pdf_info['Title'] = title
392 pdf_info['Author'] = 'Chrisu Jähnl'
393 pdf_info['Subject'] = 'Temperaturen'
394 pdf_info['Keywords'] = 'Seepark Obsteig'
395 pdf_info['CreationDate'] = datetime.datetime.now()
396 pdf_info['ModDate'] = datetime.datetime.today()
398 response = make_response(binary_pdf.getvalue())
399 response.headers['Content-Type'] = 'application/pdf'
400 response.headers['Content-Disposition'] = 'attachment; filename=seepark_{:04d}-{:02d}.pdf'.format(year, month)
406 airvalue, airtime = currentairtemperature(cityid)
407 watervalue, watertime = currentwatertemperature(mainsensor)
409 return render_template(
412 watervalue=watervalue,