6 from collections import defaultdict
7 from flask import Flask, render_template, jsonify, request, abort, Response
9 from flask_sqlalchemy import SQLAlchemy, inspect
10 from sqlalchemy import func
13 # https://stackoverflow.com/a/37350445
14 def sqlalchemy_model_to_dict(model):
15 return {c.key: getattr(model, c.key)
16 for c in inspect(model).mapper.column_attrs}
19 class JSONEncoder(flask.json.JSONEncoder):
20 def default(self, object):
21 if isinstance(object, datetime.datetime):
22 return object.isoformat()
23 elif isinstance(object, db.Model):
24 return sqlalchemy_model_to_dict(object)
25 return super().default(object)
28 def parse_datetime(date_str):
29 return datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
32 def get_sqlalchemy_database_uri(config):
33 user = config.get('database', 'user')
34 pwd = config.get('database', 'password')
35 host = config.get('database', 'hostname')
36 db = config.get('database', 'database')
37 return 'mysql+mysqldb://{}:{}@{}/{}'.format(user, pwd, host, db)
40 config = configparser.ConfigParser()
41 config.read(os.environ['SEEPARKINI'])
42 apikey = config.get('openweathermap', 'apikey')
43 cityid = config.get('openweathermap', 'cityid')
44 mainsensor = config.get('webapp', 'mainsensor')
47 app.json_encoder = JSONEncoder
48 app.config['SQLALCHEMY_DATABASE_URI'] = get_sqlalchemy_database_uri(config)
49 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
54 class Sensors(db.Model):
55 __tablename__ = 'sensors'
58 class OpenWeatherMap(db.Model):
59 __tablename__ = 'openweathermap'
62 def calc_grouping_resolution(begin, end):
63 """How many data points should be between the timestamps begin and end?"""
64 # copied from munin/master/_bin/munin-cgi-graph.in
65 # except day: 300 -> 600
72 duration = (end - begin).total_seconds()
75 resolution = resolutions['day']
76 elif duration <= 7 * day:
77 resolution = resolutions['week']
78 elif duration <= 31 * day:
79 resolution = resolutions['month']
81 resolution = resolutions['year']
85 def select_sensordata(sensor_id, sensor_type, begin, end):
87 if sensor_id is not None:
88 query = query.filter(Sensors.sensor_id == sensor_id)
89 if sensor_type is not None:
90 query = query.filter(Sensors.value_type == sensor_type)
92 query = query.filter(Sensors.timestamp >= begin)
94 query = query.filter(Sensors.timestamp <= end)
98 def select_sensordata_grouped(sensor_id, sensor_type, begin, end):
99 # determine resolution (interval in seconds for data points)
100 resolution = calc_grouping_resolution(begin, end)
102 # Let the database do the grouping. Example in SQL (MySQL):
103 # 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;
104 query = db.session.query(func.to_seconds(Sensors.timestamp).op('div')(resolution).label('g'),
105 func.from_unixtime(func.avg(func.unix_timestamp(Sensors.timestamp))).label('timestamp'),
106 func.avg(Sensors.value).label('value'),
107 Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
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 query = query.filter(Sensors.timestamp >= begin)
113 query = query.filter(Sensors.timestamp <= end)
114 query = query.group_by('g', Sensors.sensor_id, Sensors.value_type, Sensors.sensor_name)
118 def select_openweatherdata(cityid, begin, end):
119 query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
120 if begin is not None:
121 query = query.filter(OpenWeatherMap.datetime >= begin)
123 query = query.filter(OpenWeatherMap.datetime <= end)
127 def select_openweatherdata_grouped(cityid, begin, end):
128 # determine resolution (interval in seconds for data points)
129 resolution = calc_grouping_resolution(begin, end)
131 # Let the database do the grouping. Example in SQL (MySQL):
132 # 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;
133 query = db.session.query(func.to_seconds(OpenWeatherMap.datetime).op('div')(resolution).label('g'),
134 func.from_unixtime(func.avg(func.unix_timestamp(OpenWeatherMap.datetime))).label('datetime'),
135 func.avg(OpenWeatherMap.temp).label('temp'),
136 OpenWeatherMap.cityid)
137 OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
138 query = query.filter(OpenWeatherMap.datetime >= begin)
139 query = query.filter(OpenWeatherMap.datetime <= end)
140 query = query.group_by('g', OpenWeatherMap.cityid)
144 def convert_to_c3(result, id, field_x, field_y):
145 c3result = defaultdict(list)
147 c3result[str(getattr(row, id))].append(getattr(row, field_y))
148 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
149 c3result[str(getattr(row, id)) + '_x'].append(dt)
153 def request_arg(key, type, default=None):
154 """Returns the key from the request if available, otherwise the default value.
155 In case type is provided and the key is present, the value is converted by calling type.
156 In other words: Reimplement request.args.get but don't return default value if
157 type raises a ValueError."""
158 if key in request.args:
160 return type(request.args[key])
161 except ValueError as e:
162 abort(Response(str(e), 400))
167 def sensordata(sensor_id=None, sensor_type=None):
168 begin = request_arg('begin', parse_datetime)
169 end = request_arg('end', parse_datetime)
170 mode = request.args.get('mode', 'full')
171 format = request.args.get('format', 'default')
174 result = select_sensordata(sensor_id, sensor_type, begin, end)
175 elif mode == 'consolidated':
176 if begin is None or end is None:
177 abort(Response('begin and end have to be set for mode==consolidated', 400))
178 result = select_sensordata_grouped(sensor_id, sensor_type, begin, end)
180 abort(Response('unknown value for mode', 400))
183 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
187 def openweathermapdata(cityid):
188 begin = request_arg('begin', parse_datetime)
189 end = request_arg('end', parse_datetime)
190 mode = request.args.get('mode', 'full')
191 format = request.args.get('format', 'default')
194 result = select_openweatherdata(cityid, begin, end)
195 elif mode == 'consolidated':
196 if begin is None or end is None:
197 abort(Response('begin and end have to be set for mode==consolidated', 400))
198 result = select_openweatherdata_grouped(cityid, begin, end)
200 abort(Response('unknown value for mode', 400))
203 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
207 def currentairtemperature(cityid):
208 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
209 return result.temp, result.datetime
212 def currentwatertemperature(sensorid):
213 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
214 return result.value, result.timestamp
217 @app.route('/api/<version>/sensors/')
218 def sensors(version):
219 """List all sensors found in the database"""
220 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
221 return jsonify(result)
224 @app.route('/api/<version>/sensor/id/<sensor_id>')
225 def sensorid(version, sensor_id):
226 """Return all data for a specific sensor
229 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
230 end=<datetime>, optional, format like "2018-05-19T21:07:53"
231 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
232 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
234 result = sensordata(sensor_id=sensor_id)
235 return jsonify(result)
238 @app.route('/api/<version>/sensor/type/<sensor_type>')
239 def sensortype(version, sensor_type):
240 """Return all data for a specific sensor type
243 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
244 end=<datetime>, optional, format like "2018-05-19T21:07:53"
245 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
246 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
248 result = sensordata(sensor_type=sensor_type)
249 return jsonify(result)
252 @app.route('/api/<version>/openweathermap/cities')
253 def openweathermap_cities(version):
254 """List all city IDs found in the database"""
255 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
256 return jsonify(result)
259 @app.route('/api/<version>/openweathermap/city/<cityid>')
260 def openweathermap_city(version, cityid):
261 """List all data found for a city"""
262 result = openweathermapdata(cityid=cityid)
263 return jsonify(result)
266 @app.route('/api/<version>/currentairtemperature')
267 def currentair(version):
268 value, timestamp = currentairtemperature(cityid)
269 return jsonify({"value": value, "timestamp": timestamp})
272 @app.route('/api/<version>/currentwatertemperature')
273 def currentwater(version):
274 value, timestamp = currentwatertemperature(mainsensor)
275 return jsonify({"value": value, "timestamp": timestamp})
280 airvalue, airtime = currentairtemperature(cityid)
281 watervalue, watertime = currentwatertemperature(mainsensor)
283 return render_template(
286 watervalue=watervalue,