6 from collections import defaultdict
7 from flask import Flask, render_template, jsonify, request, abort, Response
9 from flask_sqlalchemy import SQLAlchemy, inspect
12 app_path = os.path.dirname(os.path.realpath(__file__))
13 lib_path = os.path.join(app_path, '..')
14 sys.path.append(lib_path)
15 from seeparklib.openweathermap import openweathermap_json, OpenWeatherMapError
18 # https://stackoverflow.com/a/37350445
19 def sqlalchemy_model_to_dict(model):
20 return {c.key: getattr(model, c.key)
21 for c in inspect(model).mapper.column_attrs}
24 class JSONEncoder(flask.json.JSONEncoder):
25 def default(self, object):
26 if isinstance(object, datetime.datetime):
27 return object.isoformat()
28 elif isinstance(object, db.Model):
29 return sqlalchemy_model_to_dict(object)
30 return super().default(object)
33 def parse_datetime(date_str):
34 return datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
37 def get_sqlalchemy_database_uri(config):
38 user = config.get('database', 'user')
39 pwd = config.get('database', 'password')
40 host = config.get('database', 'hostname')
41 db = config.get('database', 'database')
42 return 'mysql+mysqldb://{}:{}@{}/{}'.format(user, pwd, host, db)
45 config = configparser.ConfigParser()
46 config.read(os.environ['SEEPARKINI'])
47 apikey = config.get('openweathermap', 'apikey')
48 cityid = config.get('openweathermap', 'cityid')
49 mainsensor = config.get('webapp', 'mainsensor')
52 app.json_encoder = JSONEncoder
53 app.config['SQLALCHEMY_DATABASE_URI'] = get_sqlalchemy_database_uri(config)
54 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
59 class Sensors(db.Model):
60 __tablename__ = 'sensors'
63 class OpenWeatherMap(db.Model):
64 __tablename__ = 'openweathermap'
67 def select_sensordata(sensor_id, sensor_type, begin, end, mode):
69 if sensor_id is not None:
70 query = query.filter(Sensors.sensor_id == sensor_id)
71 if sensor_type is not None:
72 query = query.filter(Sensors.value_type == sensor_type)
74 query = query.filter(Sensors.timestamp >= begin)
76 query = query.filter(Sensors.timestamp <= end)
77 if mode == 'consolidated' and begin is not None and end is not None:
78 # copied from munin/master/_bin/munin-cgi-graph.in
79 # interval in seconds for data points
86 duration = (end - begin).total_seconds()
89 resolution = resolutions['day']
90 elif duration <= 7 * day:
91 resolution = resolutions['week']
92 elif duration <= 31 * day:
93 resolution = resolutions['month']
95 resolution = resolutions['year']
96 # TODO: filter out samples from 'result'
98 # 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;
102 def select_openweatherdata(cityid, begin, end, mode):
103 query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
104 if begin is not None:
105 query = query.filter(OpenWeatherMap.datetime >= begin)
107 query = query.filter(OpenWeatherMap.datetime <= end)
108 if mode == 'consolidated' and begin is not None and end is not None:
109 # copied from munin/master/_bin/munin-cgi-graph.in
110 # interval in seconds for data points
117 duration = (end - begin).total_seconds()
120 resolution = resolutions['day']
121 elif duration < 7 * day:
122 resolution = resolutions['week']
123 elif duration < 31 * day:
124 resolution = resolutions['month']
126 resolution = resolutions['year']
127 # TODO: filter out samples from 'result'
129 # 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 def convert_to_c3(result, id, field_x, field_y):
134 c3result = defaultdict(list)
136 c3result[getattr(row, id)].append(getattr(row, field_y))
137 dt = getattr(row, field_x).strftime('%Y-%m-%d %H:%M:%S')
138 c3result[str(getattr(row, id)) + '_x'].append(dt)
142 def request_arg(key, type, default=None):
143 """Returns the key from the request if available, otherwise the default value.
144 In case type is provided and the key is present, the value is converted by calling type.
145 In other words: Reimplement request.args.get but don't return default value if
146 type raises a ValueError."""
147 if key in request.args:
149 return type(request.args[key])
150 except ValueError as e:
151 abort(Response(str(e), 400))
156 def sensordata(sensor_id=None, sensor_type=None):
157 begin = request_arg('begin', parse_datetime)
158 end = request_arg('end', parse_datetime)
159 mode = request.args.get('mode', 'full')
160 format = request.args.get('format', 'default')
162 result = select_sensordata(sensor_id, sensor_type, begin, end, mode)
165 return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
169 def openweathermapdata(cityid):
170 begin = request_arg('begin', parse_datetime)
171 end = request_arg('end', parse_datetime)
172 mode = request.args.get('mode', 'full')
173 format = request.args.get('format', 'default')
175 result = select_openweatherdata(cityid, begin, end, mode)
178 return convert_to_c3(result, 'cityid', 'datetime', 'temp')
182 def currentairtemperature(cityid):
183 result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
184 return result.temp, result.datetime
187 def currentwatertemperature(sensorid):
188 result = Sensors.query.filter_by(sensor_id=sensorid).order_by(Sensors.timestamp.desc()).first()
189 return result.value, result.timestamp
192 @app.route('/api/<version>/sensors/')
193 def sensors(version):
194 """List all sensors found in the database"""
195 result = db.session.query(Sensors.sensor_id, Sensors.sensor_name, Sensors.value_type).distinct().all()
196 return jsonify(result)
199 @app.route('/api/<version>/sensor/id/<sensor_id>')
200 def sensorid(version, sensor_id):
201 """Return all data for a specific sensor
204 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
205 end=<datetime>, optional, format like "2018-05-19T21:07:53"
206 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
207 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
209 result = sensordata(sensor_id=sensor_id)
210 return jsonify(result)
213 @app.route('/api/<version>/sensor/type/<sensor_type>')
214 def sensortype(version, sensor_type):
215 """Return all data for a specific sensor type
218 begin=<datetime>, optional, format like "2018-05-19T21:07:53"
219 end=<datetime>, optional, format like "2018-05-19T21:07:53"
220 mode=<full|consolidated>, optional. return all rows (default) or with lower resolution (for charts)
221 format=<default|c3>, optional. return result as returned by sqlalchemy (default) or formatted for c3.js
223 result = sensordata(sensor_type=sensor_type)
224 return jsonify(result)
227 @app.route('/api/<version>/openweathermap/cities')
228 def openweathermap_cities(version):
229 """List all city IDs found in the database"""
230 result = db.session.query(OpenWeatherMap.cityid).distinct().all()
231 return jsonify(result)
234 @app.route('/api/<version>/openweathermap/city/<cityid>')
235 def openweathermap_city(version, cityid):
236 """List all data found for a city"""
237 result = openweathermapdata(cityid=cityid)
238 return jsonify(result)
243 airvalue, airtime = currentairtemperature(cityid)
244 watervalue, watertime = currentwatertemperature(mainsensor)
246 return render_template(
249 watervalue=watervalue,