trailing whitespace
[chrisu/seepark.git] / web / seepark_web.py
1 import datetime
2 import time
3 import configparser
4 import os
5 import sys
6 from collections import defaultdict
7 from flask import Flask, render_template, jsonify, request, abort, Response
8 import flask.json
9 from flask_sqlalchemy import SQLAlchemy, inspect
10
11
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
16
17
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}
22
23
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)
31
32
33 def parse_datetime(date_str):
34     return datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
35
36
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)
43
44
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('temperature', 'mainsensor')
50
51 app = Flask(__name__)
52 app.json_encoder = JSONEncoder
53 app.config['SQLALCHEMY_DATABASE_URI'] = get_sqlalchemy_database_uri(config)
54 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
55 db = SQLAlchemy(app)
56 db.reflect(app=app)
57
58
59 class Sensors(db.Model):
60     __tablename__ = 'sensors'
61
62
63 class OpenWeatherMap(db.Model):
64     __tablename__ = 'openweathermap'
65
66
67 def select_sensordata(sensor_id, sensor_type, begin, end, mode):
68     query = Sensors.query
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)
73     if begin is not None:
74         query = query.filter(Sensors.timestamp >= begin)
75     if end is not None:
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
80         resolutions = dict(
81             day   =   300,
82             week  =  1800,
83             month =  7200,
84             year  = 86400,
85         )
86         duration = (end - begin).total_seconds()
87         day = 60 * 60 * 24
88         if duration <= day:
89             resolution = resolutions['day']
90         elif duration <= 7 * day:
91             resolution = resolutions['week']
92         elif duration <= 31 * day:
93             resolution = resolutions['month']
94         else:
95             resolution = resolutions['year']
96         # TODO: filter out samples from 'result'
97         # something like
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;
99     return query.all()
100
101
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)
106     if end is not None:
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
111         resolutions = dict(
112             day   =   300,
113             week  =  1800,
114             month =  7200,
115             year  = 86400,
116         )
117         duration = (end - begin).total_seconds()
118         day = 60 * 60 * 24
119         if duration < day:
120             resolution = resolutions['day']
121         elif duration < 7 * day:
122             resolution = resolutions['week']
123         elif duration < 31 * day:
124             resolution = resolutions['month']
125         else:
126             resolution = resolutions['year']
127         # TODO: filter out samples from 'result'
128         # something like 
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;
130     return query.all()
131
132
133 def convert_to_c3(result, id, field_x, field_y):
134     c3result = defaultdict(list)
135     for row in result:
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)
139     return c3result
140
141
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:
148         try:
149             return type(request.args[key])
150         except ValueError as e:
151             abort(Response(str(e), 400))
152     else:
153         return default
154
155
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')
161
162     result = select_sensordata(sensor_id, sensor_type, begin, end, mode)
163
164     if format == 'c3':
165         return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
166     return result
167
168
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')
174
175     result = select_openweatherdata(cityid, begin, end, mode)
176
177     if format == 'c3':
178         return convert_to_c3(result, 'cityid', 'datetime', 'temp')
179     return result
180
181
182 def currentairtemperature(cityid):
183     result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
184     return result.temp, result.datetime
185
186
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
190
191
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)
197
198
199 @app.route('/api/<version>/sensor/id/<sensor_id>')
200 def sensorid(version, sensor_id):
201     """Return all data for a specific sensor
202
203     URL parameters:
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
208     """
209     result = sensordata(sensor_id=sensor_id)
210     return jsonify(result)
211
212
213 @app.route('/api/<version>/sensor/type/<sensor_type>')
214 def sensortype(version, sensor_type):
215     """Return all data for a specific sensor type
216
217     URL parameters:
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
222     """
223     result = sensordata(sensor_type=sensor_type)
224     return jsonify(result)
225
226
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)
232
233
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)
239
240
241 @app.route("/")
242 def index():
243     airvalue, airtime     = currentairtemperature(cityid)
244     watervalue, watertime = currentwatertemperature(mainsensor)
245
246     return render_template(
247         'seepark_web.html',
248         apikey=apikey,
249         watervalue=watervalue,
250         watertime=watertime,
251         airvalue=airvalue,
252         airtime=airtime,
253     )