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
34 # https://stackoverflow.com/a/37350445
35 def sqlalchemy_model_to_dict(model):
36 return {c.key: getattr(model, c.key)
37 for c in inspect(model).mapper.column_attrs}
40 class JSONEncoder(flask.json.JSONEncoder):
41 def default(self, object):
42 if isinstance(object, datetime.datetime):
43 return object.isoformat()
44 elif isinstance(object, db.Model):
45 return sqlalchemy_model_to_dict(object)
46 return super().default(object)
49 def parse_datetime(date_str):
50 return datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
53 def get_sqlalchemy_database_uri(config):
54 user = config.get('database', 'user')
55 pwd = config.get('database', 'password')
56 host = config.get('database', 'hostname')
57 db = config.get('database', 'database')
58 return 'mysql+mysqldb://{}:{}@{}/{}'.format(user, pwd, host, db)
61 config = configparser.ConfigParser()
62 config.read(os.environ['SEEPARKINI'])
63 apikey = config.get('openweathermap', 'apikey')
64 cityid = config.get('openweathermap', 'cityid')
65 mainsensor = config.get('webapp', 'mainsensor')
68 app.json_encoder = JSONEncoder
69 app.config['SQLALCHEMY_DATABASE_URI'] = get_sqlalchemy_database_uri(config)
70 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
75 class Sensors(db.Model):
76 __tablename__ = 'sensors'
79 class OpenWeatherMap(db.Model):
80 __tablename__ = 'openweathermap'
83 def calc_grouping_resolution(begin, end):
84 """How many data points should be between the timestamps begin and end?"""
85 # copied from munin/master/_bin/munin-cgi-graph.in
86 # except day: 300 -> 600
93 duration = (end - begin).total_seconds()
96 resolution = resolutions['day']
97 elif duration <= 7 * day:
98 resolution = resolutions['week']
99 elif duration <= 31 * day:
100 resolution = resolutions['month']
102 resolution = resolutions['year']
106 def select_sensordata(sensor_id, sensor_type, begin, end):
107 query = Sensors.query
108 if sensor_id is not None:
109 query = query.filter(Sensors.sensor_id == sensor_id)
110 if sensor_type is not None:
111 query = query.filter(Sensors.value_type == sensor_type)
112 if begin is not None:
113 query = query.filter(Sensors.timestamp >= begin)
115 query = query.filter(Sensors.timestamp <= end)
119 def select_sensordata_grouped(sensor_id, sensor_type, begin, end):
120 # determine resolution (interval in seconds for data points)
121 resolution = calc_grouping_resolution(begin, end)
123 # Let the database do the grouping. Example in SQL (MySQL):
124 # 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;
125 query = db.session.query(func.to_seconds(Sensors.timestamp).op('div')(resolution).label('g'),
126 func.from_unixtime(func.avg(func.unix_timestamp(Sensors.timestamp))).label('timestamp'),
127 func.avg(Sensors.value).label('value'),
128 Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
129 if sensor_id is not None:
130 query = query.filter(Sensors.sensor_id == sensor_id)
131 if sensor_type is not None:
132 query = query.filter(Sensors.value_type == sensor_type)
133 query = query.filter(Sensors.timestamp >= begin)
134 query = query.filter(Sensors.timestamp <= end)
135 query = query.group_by('g', Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
139 def select_openweatherdata(cityid, begin, end):
140 query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
141 if begin is not None:
142 query = query.filter(OpenWeatherMap.datetime >= begin)
144 query = query.filter(OpenWeatherMap.datetime <= end)
148 def select_openweatherdata_grouped(cityid, begin, end):
149 # determine resolution (interval in seconds for data points)
150 resolution = calc_grouping_resolution(begin, end)
152 # Let the database do the grouping. Example in SQL (MySQL):
153 # 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;
154 query = db.session.query(func.to_seconds(OpenWeatherMap.datetime).op('div')(resolution).label('g'),
155 func.from_unixtime(func.avg(func.unix_timestamp(OpenWeatherMap.datetime))).label('datetime'),
156 func.avg(OpenWeatherMap.temp).label('temp'),
157 OpenWeatherMap.cityid)
158 OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
159 query = query.filter(OpenWeatherMap.datetime >= begin)
160 query = query.filter(OpenWeatherMap.datetime <= end)
161 query = query.group_by('g', OpenWeatherMap.cityid)
165 def convert_to_c3(result, id, field_x, field_y):
166 c3result = defaultdict(list)
168 c3result[str(getattr(row, id))].append(getattr(row, field_y))
169 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
170 c3result[str(getattr(row, id)) + '_x'].append(dt)
174 def request_arg(key, type, default=None):
175 """Returns the key from the request if available, otherwise the default value.
176 In case type is provided and the key is present, the value is converted by calling type.
177 In other words: Reimplement request.args.get but don't return default value if
178 type raises a ValueError."""
179 if key in request.args:
181 return type(request.args[key])
182 except ValueError as e:
183 abort(Response(str(e), 400))
188 def sensordata(sensor_id=None, sensor_type=None):
189 begin = request_arg('begin', parse_datetime)
190 end = request_arg('end', parse_datetime)
191 mode = request.args.get('mode', 'full')
192 format = request.args.get('format', 'default')
195 result = select_sensordata(sensor_id, sensor_type, begin, end)
196 elif mode == 'consolidated':
197 if begin is None or end is None:
198 abort(Response('begin and end have to be set for mode==consolidated', 400))
199 result = select_sensordata_grouped(sensor_id, sensor_type, begin, end)
201 abort(Response('unknown value for mode', 400))
204 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
208 def openweathermapdata(cityid):
209 begin = request_arg('begin', parse_datetime)
210 end = request_arg('end', parse_datetime)
211 mode = request.args.get('mode', 'full')
212 format = request.args.get('format', 'default')
215 result = select_openweatherdata(cityid, begin, end)
216 elif mode == 'consolidated':
217 if begin is None or end is None:
218 abort(Response('begin and end have to be set for mode==consolidated', 400))
219 result = select_openweatherdata_grouped(cityid, begin, end)
221 abort(Response('unknown value for mode', 400))
224 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
228 def currentairtemperature(cityid):
229 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
230 return result.temp, result.datetime
233 def currentwatertemperature(sensorid):
234 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
235 return result.value, result.timestamp
239 return (date + datetime.timedelta(days=42)).replace(day=1)
242 @app.route('/api/<version>/sensors/')
243 def sensors(version):
244 """List all sensors found in the database"""
245 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
246 return jsonify(result)
249 @app.route('/api/<version>/sensor/id/<sensor_id>')
250 def sensorid(version, sensor_id):
251 """Return all data for a specific sensor
254 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
255 end=<datetime>, optional, format like "2018-05-19T21:07:53"
256 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
257 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
259 result = sensordata(sensor_id=sensor_id)
260 return jsonify(result)
263 @app.route('/api/<version>/sensor/type/<sensor_type>')
264 def sensortype(version, sensor_type):
265 """Return all data for a specific sensor type
268 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
269 end=<datetime>, optional, format like "2018-05-19T21:07:53"
270 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
271 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
273 result = sensordata(sensor_type=sensor_type)
274 return jsonify(result)
277 @app.route('/api/<version>/openweathermap/cities')
278 def openweathermap_cities(version):
279 """List all city IDs found in the database"""
280 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
281 return jsonify(result)
284 @app.route('/api/<version>/openweathermap/city/<cityid>')
285 def openweathermap_city(version, cityid):
286 """List all data found for a city"""
287 result = openweathermapdata(cityid=cityid)
288 return jsonify(result)
291 @app.route('/api/<version>/currentairtemperature')
292 def currentair(version):
293 value, timestamp = currentairtemperature(cityid)
294 return jsonify({"value": value, "timestamp": timestamp})
297 @app.route('/api/<version>/currentwatertemperature')
298 def currentwater(version):
299 value, timestamp = currentwatertemperature(mainsensor)
300 return jsonify({"value": value, "timestamp": timestamp})
303 @app.route('/report/<int:year>-<int:month>')
304 def report(year, month):
306 begin = datetime.datetime(year, month, 1)
307 end = add_month(begin)
308 data = list(select_sensordata_grouped(mainsensor, 'Wassertemperatur', begin, end))
309 x = [d.timestamp for d in data]
310 y = [d.value for d in data]
315 days_datetime.append(d)
316 d = d + datetime.timedelta(1)
317 days_str = [d.strftime('%d') for d in days_datetime]
319 binary_pdf = io.BytesIO()
320 with PdfPages(binary_pdf) as pdf:
321 a4 = (21./2.54, 29.7/2.54)
322 plt.figure(figsize=a4)
324 plt.xticks(days_datetime, days_str, rotation='vertical')
326 plt.ylabel('Temparatur in °C')
327 plt.axis(xmin=begin, xmax=end)
329 title = 'Seepark Wassertemperatur {} {}'.format(MONTH_DE[begin.month-1], begin.year)
333 pdf_info = pdf.infodict()
334 pdf_info['Title'] = title
335 pdf_info['Author'] = 'Chrisu Jähnl'
336 pdf_info['Subject'] = 'Wassertemperatur'
337 pdf_info['Keywords'] = 'Seepark Wassertemperatur'
338 pdf_info['CreationDate'] = datetime.datetime.now()
339 pdf_info['ModDate'] = datetime.datetime.today()
341 response = make_response(binary_pdf.getvalue())
342 response.headers['Content-Type'] = 'application/pdf'
343 response.headers['Content-Disposition'] = 'attachment; filename=seepark_{:04d}-{:02d}.pdf'.format(year, month)
349 airvalue, airtime = currentairtemperature(cityid)
350 watervalue, watertime = currentwatertemperature(mainsensor)
352 return render_template(
355 watervalue=watervalue,