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;
96 def select_openweatherdata(cityid, begin, end, mode):
97 query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
99 query = query.filter(OpenWeatherMap.datetime >= begin)
101 query = query.filter(OpenWeatherMap.datetime <= end)
102 if mode == 'consolidated' and begin is not None and end is not None:
103 # copied from munin/master/_bin/munin-cgi-graph.in
104 # interval in seconds for data points
111 duration = (end - begin).total_seconds()
114 resolution = resolutions['day']
115 elif duration < 7 * day:
116 resolution = resolutions['week']
117 elif duration < 31 * day:
118 resolution = resolutions['month']
120 resolution = resolutions['year']
121 # TODO: filter out samples from 'result'
123 # 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;
127 def convert_to_c3(result, id, field_x, field_y):
128 c3result = defaultdict(list)
130 c3result[getattr(row, id)].append(getattr(row, field_y))
131 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
132 c3result[str(getattr(row, id)) + '_x'].append(dt)
136 def request_arg(key, type, default=None):
137 """Returns the key from the request if available, otherwise the default value.
138 In case type is provided and the key is present, the value is converted by calling type.
139 In other words: Reimplement request.args.get but don't return default value if
140 type raises a ValueError."""
141 if key in request.args:
143 return type(request.args[key])
144 except ValueError as e:
145 abort(Response(str(e), 400))
150 def sensordata(sensor_id=None, sensor_type=None):
151 begin = request_arg('begin', parse_datetime)
152 end = request_arg('end', parse_datetime)
153 mode = request.args.get('mode', 'full')
154 format = request.args.get('format', 'default')
156 result = select_sensordata(sensor_id, sensor_type, begin, end, mode)
159 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
163 def openweathermapdata(cityid):
164 begin = request_arg('begin', parse_datetime)
165 end = request_arg('end', parse_datetime)
166 mode = request.args.get('mode', 'full')
167 format = request.args.get('format', 'default')
169 result = select_openweatherdata(cityid, begin, end, mode)
172 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
176 def currentairtemperature(cityid):
177 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
178 return result.temp, result.datetime
181 def currentwatertemperature(sensorid):
182 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
183 return result.value, result.timestamp
186 @app.route('/api/<version>/sensors/')
187 def sensors(version):
188 """List all sensors found in the database"""
189 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
190 return jsonify(result)
193 @app.route('/api/<version>/sensor/id/<sensor_id>')
194 def sensorid(version, sensor_id):
195 """Return all data for a specific sensor
198 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
199 end=<datetime>, optional, format like "2018-05-19T21:07:53"
200 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
201 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
203 result = sensordata(sensor_id=sensor_id)
204 return jsonify(result)
207 @app.route('/api/<version>/sensor/type/<sensor_type>')
208 def sensortype(version, sensor_type):
209 """Return all data for a specific sensor type
212 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
213 end=<datetime>, optional, format like "2018-05-19T21:07:53"
214 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
215 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
217 result = sensordata(sensor_type=sensor_type)
218 return jsonify(result)
221 @app.route('/api/<version>/openweathermap/cities')
222 def openweathermap_cities(version):
223 """List all city IDs found in the database"""
224 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
225 return jsonify(result)
228 @app.route('/api/<version>/openweathermap/city/<cityid>')
229 def openweathermap_city(version, cityid):
230 """List all data found for a city"""
231 result = openweathermapdata(cityid=cityid)
232 return jsonify(result)
237 airvalue, airtime = currentairtemperature(cityid)
238 watervalue, watertime = currentwatertemperature(mainsensor)
240 return render_template(
243 watervalue=watervalue,