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 select_sensordata_grouped(sensor_id, sensor_type, begin, end):
129 # determine resolution (interval in seconds for data points)
130 resolution = calc_grouping_resolution(begin, end)
132 # Let the database do the grouping. Example in SQL (MySQL):
133 # 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;
134 query = db.session.query(func.to_seconds(Sensors.timestamp).op('div')(resolution).label('g'),
135 func.from_unixtime(func.avg(func.unix_timestamp(Sensors.timestamp))).label('timestamp'),
136 func.avg(Sensors.value).label('value'),
137 Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
138 if sensor_id is not None:
139 query = query.filter(Sensors.sensor_id == sensor_id)
140 if sensor_type is not None:
141 query = query.filter(Sensors.value_type == sensor_type)
142 query = query.filter(Sensors.timestamp >= begin)
143 query = query.filter(Sensors.timestamp <= end)
144 query = query.group_by('g', Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
148 def select_openweatherdata(cityid, begin, end):
149 query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
150 if begin is not None:
151 query = query.filter(OpenWeatherMap.datetime >= begin)
153 query = query.filter(OpenWeatherMap.datetime <= end)
157 def select_openweatherdata_grouped(cityid, begin, end):
158 # determine resolution (interval in seconds for data points)
159 resolution = calc_grouping_resolution(begin, end)
161 # Let the database do the grouping. Example in SQL (MySQL):
162 # 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;
163 query = db.session.query(func.to_seconds(OpenWeatherMap.datetime).op('div')(resolution).label('g'),
164 func.from_unixtime(func.avg(func.unix_timestamp(OpenWeatherMap.datetime))).label('datetime'),
165 func.avg(OpenWeatherMap.temp).label('temp'),
166 OpenWeatherMap.cityid)
167 OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
168 query = query.filter(OpenWeatherMap.datetime >= begin)
169 query = query.filter(OpenWeatherMap.datetime <= end)
170 query = query.group_by('g', OpenWeatherMap.cityid)
174 def convert_to_c3(result, id, field_x, field_y):
175 c3result = defaultdict(list)
177 c3result[str(getattr(row, id))].append(getattr(row, field_y))
178 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
179 c3result[str(getattr(row, id)) + '_x'].append(dt)
183 def request_arg(key, type, default=None):
184 """Returns the key from the request if available, otherwise the default value.
185 In case type is provided and the key is present, the value is converted by calling type.
186 In other words: Reimplement request.args.get but don't return default value if
187 type raises a ValueError."""
188 if key in request.args:
190 return type(request.args[key])
191 except ValueError as e:
192 abort(Response(str(e), 400))
197 def sensordata(sensor_id=None, sensor_type=None):
198 begin = request_arg('begin', parse_datetime)
199 end = request_arg('end', parse_datetime)
200 mode = request.args.get('mode', 'full')
201 format = request.args.get('format', 'default')
204 result = select_sensordata(sensor_id, sensor_type, begin, end)
205 elif mode == 'consolidated':
206 if begin is None or end is None:
207 abort(Response('begin and end have to be set for mode==consolidated', 400))
208 result = select_sensordata_grouped(sensor_id, sensor_type, begin, end)
210 abort(Response('unknown value for mode', 400))
213 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
217 def openweathermapdata(cityid):
218 begin = request_arg('begin', parse_datetime)
219 end = request_arg('end', parse_datetime)
220 mode = request.args.get('mode', 'full')
221 format = request.args.get('format', 'default')
224 result = select_openweatherdata(cityid, begin, end)
225 elif mode == 'consolidated':
226 if begin is None or end is None:
227 abort(Response('begin and end have to be set for mode==consolidated', 400))
228 result = select_openweatherdata_grouped(cityid, begin, end)
230 abort(Response('unknown value for mode', 400))
233 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
237 def currentairtemperature(cityid):
238 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
239 return result.temp, result.datetime
242 def currentwatertemperature(sensorid):
243 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
244 return result.value, result.timestamp
248 return (date + datetime.timedelta(days=42)).replace(day=1)
251 @app.route('/api/<version>/sensors/')
252 def sensors(version):
253 """List all sensors found in the database"""
254 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
255 return jsonify(result)
258 @app.route('/api/<version>/sensor/id/<sensor_id>')
259 def sensorid(version, sensor_id):
260 """Return all data for a specific sensor
263 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
264 end=<datetime>, optional, format like "2018-05-19T21:07:53"
265 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
266 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
268 result = sensordata(sensor_id=sensor_id)
269 return jsonify(result)
272 @app.route('/api/<version>/sensor/type/<sensor_type>')
273 def sensortype(version, sensor_type):
274 """Return all data for a specific sensor type
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_type=sensor_type)
283 return jsonify(result)
286 @app.route('/api/<version>/openweathermap/cities')
287 def openweathermap_cities(version):
288 """List all city IDs found in the database"""
289 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
290 return jsonify(result)
293 @app.route('/api/<version>/openweathermap/city/<cityid>')
294 def openweathermap_city(version, cityid):
295 """List all data found for a city"""
296 result = openweathermapdata(cityid=cityid)
297 return jsonify(result)
300 @app.route('/api/<version>/currentairtemperature')
301 def currentair(version):
302 value, timestamp = currentairtemperature(cityid)
303 return jsonify({"value": value, "timestamp": timestamp})
306 @app.route('/api/<version>/currentwatertemperature')
307 def currentwater(version):
308 value, timestamp = currentwatertemperature(mainsensor)
309 return jsonify({"value": value, "timestamp": timestamp})
312 @app.route('/report/<int:year>-<int:month>')
313 def report(year, month):
315 begin = datetime.datetime(year, month, 1)
316 end = add_month(begin)
317 data = list(select_sensordata(mainsensor, 'Wassertemperatur', begin, end))
318 x = np.array([d.timestamp for d in data])
319 y = np.array([d.value for d in data])
324 days_datetime.append(d)
325 d = d + datetime.timedelta(1)
326 days_str = [d.strftime('%d') for d in days_datetime]
328 binary_pdf = io.BytesIO()
329 with PdfPages(binary_pdf) as pdf:
330 a4 = (21./2.54, 29.7/2.54)
331 title = 'Seepark Wassertemperatur {} {}'.format(MONTH_DE[begin.month-1], begin.year)
332 report_times = [datetime.time(10), datetime.time(15)]
335 plt.figure(figsize=a4)
337 plt.ylabel('Temperatur in °C')
338 for t in report_times:
339 columns.append('Wassertemperatur {} Uhr'.format(t.hour))
341 for d in days_datetime:
342 cell = ['{}, {}. {}'.format(DAY_OF_WEEK_DE[d.weekday()], d.day, MONTH_DE[d.month-1])]
343 for t in report_times:
344 report_datetime = datetime.datetime.combine(d.date(), t)
345 closest_index = np.argmin(np.abs(x - report_datetime))
346 if abs(x[closest_index] - report_datetime) > datetime.timedelta(hours=1):
349 value = y[closest_index]
350 cell.append('{:.1f}° C'.format(value))
354 ax.table(cellText=cells, colLabels=columns,
358 plt.subplots_adjust(left=0.1, right=0.9) # do not cut row labels
362 plt.figure(figsize=a4)
364 plt.xticks(days_datetime, days_str, rotation='vertical')
366 plt.axis(xmin=begin, xmax=end)
371 pdf_info = pdf.infodict()
372 pdf_info['Title'] = title
373 pdf_info['Author'] = 'Chrisu Jähnl'
374 pdf_info['Subject'] = 'Wassertemperatur'
375 pdf_info['Keywords'] = 'Seepark Wassertemperatur'
376 pdf_info['CreationDate'] = datetime.datetime.now()
377 pdf_info['ModDate'] = datetime.datetime.today()
379 response = make_response(binary_pdf.getvalue())
380 response.headers['Content-Type'] = 'application/pdf'
381 response.headers['Content-Disposition'] = 'attachment; filename=seepark_{:04d}-{:02d}.pdf'.format(year, month)
387 airvalue, airtime = currentairtemperature(cityid)
388 watervalue, watertime = currentwatertemperature(mainsensor)
390 return render_template(
393 watervalue=watervalue,