6 from collections import defaultdict
7 from flask import Flask, render_template, jsonify, request, abort, Response
9 from flask_sqlalchemy import SQLAlchemy, inspect
12 # https://stackoverflow.com/a/37350445
13 def sqlalchemy_model_to_dict(model):
14 return {c.key: getattr(model, c.key)
15 for c in inspect(model).mapper.column_attrs}
18 class JSONEncoder(flask.json.JSONEncoder):
19 def default(self, object):
20 if isinstance(object, datetime.datetime):
21 return object.isoformat()
22 elif isinstance(object, db.Model):
23 return sqlalchemy_model_to_dict(object)
24 return super().default(object)
27 def parse_datetime(date_str):
28 return datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
31 def get_sqlalchemy_database_uri(config):
32 user = config.get('database', 'user')
33 pwd = config.get('database', 'password')
34 host = config.get('database', 'hostname')
35 db = config.get('database', 'database')
36 return 'mysql+mysqldb://{}:{}@{}/{}'.format(user, pwd, host, db)
39 config = configparser.ConfigParser()
40 config.read(os.environ['SEEPARKINI'])
41 apikey = config.get('openweathermap', 'apikey')
42 cityid = config.get('openweathermap', 'cityid')
43 mainsensor = config.get('webapp', 'mainsensor')
46 app.json_encoder = JSONEncoder
47 app.config['SQLALCHEMY_DATABASE_URI'] = get_sqlalchemy_database_uri(config)
48 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
53 class Sensors(db.Model):
54 __tablename__ = 'sensors'
57 class OpenWeatherMap(db.Model):
58 __tablename__ = 'openweathermap'
61 def select_sensordata(sensor_id, sensor_type, begin, end, mode):
63 if sensor_id is not None:
64 query = query.filter(Sensors.sensor_id == sensor_id)
65 if sensor_type is not None:
66 query = query.filter(Sensors.value_type == sensor_type)
68 query = query.filter(Sensors.timestamp >= begin)
70 query = query.filter(Sensors.timestamp <= end)
71 if mode == 'consolidated' and begin is not None and end is not None:
72 # copied from munin/master/_bin/munin-cgi-graph.in
73 # interval in seconds for data points
80 duration = (end - begin).total_seconds()
83 resolution = resolutions['day']
84 elif duration <= 7 * day:
85 resolution = resolutions['week']
86 elif duration <= 31 * day:
87 resolution = resolutions['month']
89 resolution = resolutions['year']
90 # TODO: filter out samples from 'result'
92 # 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;
93 # seepark_web.db.session.query(func.to_seconds(Sensors.timestamp).op('div')(60*60*24).label('g'), func.min(Sensors.timestamp), func.min(Sensors.value)).group_by('g').all()
97 def select_openweatherdata(cityid, begin, end, mode):
98 query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
100 query = query.filter(OpenWeatherMap.datetime >= begin)
102 query = query.filter(OpenWeatherMap.datetime <= end)
103 if mode == 'consolidated' and begin is not None and end is not None:
104 # copied from munin/master/_bin/munin-cgi-graph.in
105 # interval in seconds for data points
112 duration = (end - begin).total_seconds()
115 resolution = resolutions['day']
116 elif duration < 7 * day:
117 resolution = resolutions['week']
118 elif duration < 31 * day:
119 resolution = resolutions['month']
121 resolution = resolutions['year']
122 # TODO: filter out samples from 'result'
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;
128 def convert_to_c3(result, id, field_x, field_y):
129 c3result = defaultdict(list)
131 c3result[getattr(row, id)].append(getattr(row, field_y))
132 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
133 c3result[str(getattr(row, id)) + '_x'].append(dt)
137 def request_arg(key, type, default=None):
138 """Returns the key from the request if available, otherwise the default value.
139 In case type is provided and the key is present, the value is converted by calling type.
140 In other words: Reimplement request.args.get but don't return default value if
141 type raises a ValueError."""
142 if key in request.args:
144 return type(request.args[key])
145 except ValueError as e:
146 abort(Response(str(e), 400))
151 def sensordata(sensor_id=None, sensor_type=None):
152 begin = request_arg('begin', parse_datetime)
153 end = request_arg('end', parse_datetime)
154 mode = request.args.get('mode', 'full')
155 format = request.args.get('format', 'default')
157 result = select_sensordata(sensor_id, sensor_type, begin, end, mode)
160 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
164 def openweathermapdata(cityid):
165 begin = request_arg('begin', parse_datetime)
166 end = request_arg('end', parse_datetime)
167 mode = request.args.get('mode', 'full')
168 format = request.args.get('format', 'default')
170 result = select_openweatherdata(cityid, begin, end, mode)
173 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
177 def currentairtemperature(cityid):
178 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
179 return result.temp, result.datetime
182 def currentwatertemperature(sensorid):
183 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
184 return result.value, result.timestamp
187 @app.route('/api/<version>/sensors/')
188 def sensors(version):
189 """List all sensors found in the database"""
190 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
191 return jsonify(result)
194 @app.route('/api/<version>/sensor/id/<sensor_id>')
195 def sensorid(version, sensor_id):
196 """Return all data for a specific sensor
199 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
200 end=<datetime>, optional, format like "2018-05-19T21:07:53"
201 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
202 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
204 result = sensordata(sensor_id=sensor_id)
205 return jsonify(result)
208 @app.route('/api/<version>/sensor/type/<sensor_type>')
209 def sensortype(version, sensor_type):
210 """Return all data for a specific sensor type
213 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
214 end=<datetime>, optional, format like "2018-05-19T21:07:53"
215 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
216 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
218 result = sensordata(sensor_type=sensor_type)
219 return jsonify(result)
222 @app.route('/api/<version>/openweathermap/cities')
223 def openweathermap_cities(version):
224 """List all city IDs found in the database"""
225 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
226 return jsonify(result)
229 @app.route('/api/<version>/openweathermap/city/<cityid>')
230 def openweathermap_city(version, cityid):
231 """List all data found for a city"""
232 result = openweathermapdata(cityid=cityid)
233 return jsonify(result)
238 airvalue, airtime = currentairtemperature(cityid)
239 watervalue, watertime = currentwatertemperature(mainsensor)
241 return render_template(
244 watervalue=watervalue,