remove import of our seeparklib
[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 # 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}
16
17
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)
25
26
27 def parse_datetime(date_str):
28     return datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
29
30
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)
37
38
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')
44
45 app = Flask(__name__)
46 app.json_encoder = JSONEncoder
47 app.config['SQLALCHEMY_DATABASE_URI'] = get_sqlalchemy_database_uri(config)
48 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
49 db = SQLAlchemy(app)
50 db.reflect(app=app)
51
52
53 class Sensors(db.Model):
54     __tablename__ = 'sensors'
55
56
57 class OpenWeatherMap(db.Model):
58     __tablename__ = 'openweathermap'
59
60
61 def select_sensordata(sensor_id, sensor_type, begin, end, mode):
62     query = Sensors.query
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)
67     if begin is not None:
68         query = query.filter(Sensors.timestamp >= begin)
69     if end is not None:
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
74         resolutions = dict(
75             day   =   300,
76             week  =  1800,
77             month =  7200,
78             year  = 86400,
79         )
80         duration = (end - begin).total_seconds()
81         day = 60 * 60 * 24
82         if duration <= day:
83             resolution = resolutions['day']
84         elif duration <= 7 * day:
85             resolution = resolutions['week']
86         elif duration <= 31 * day:
87             resolution = resolutions['month']
88         else:
89             resolution = resolutions['year']
90         # TODO: filter out samples from 'result'
91         # something like
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     return query.all()
94
95
96 def select_openweatherdata(cityid, begin, end, mode):
97     query = OpenWeatherMap.query.filter(OpenWeatherMap.cityid == cityid)
98     if begin is not None:
99         query = query.filter(OpenWeatherMap.datetime >= begin)
100     if end is not None:
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
105         resolutions = dict(
106             day   =   300,
107             week  =  1800,
108             month =  7200,
109             year  = 86400,
110         )
111         duration = (end - begin).total_seconds()
112         day = 60 * 60 * 24
113         if duration < day:
114             resolution = resolutions['day']
115         elif duration < 7 * day:
116             resolution = resolutions['week']
117         elif duration < 31 * day:
118             resolution = resolutions['month']
119         else:
120             resolution = resolutions['year']
121         # TODO: filter out samples from 'result'
122         # something like 
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;
124     return query.all()
125
126
127 def convert_to_c3(result, id, field_x, field_y):
128     c3result = defaultdict(list)
129     for row in result:
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)
133     return c3result
134
135
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:
142         try:
143             return type(request.args[key])
144         except ValueError as e:
145             abort(Response(str(e), 400))
146     else:
147         return default
148
149
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')
155
156     result = select_sensordata(sensor_id, sensor_type, begin, end, mode)
157
158     if format == 'c3':
159         return convert_to_c3(result, 'sensor_id', 'timestamp', 'value')
160     return result
161
162
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')
168
169     result = select_openweatherdata(cityid, begin, end, mode)
170
171     if format == 'c3':
172         return convert_to_c3(result, 'cityid', 'datetime', 'temp')
173     return result
174
175
176 def currentairtemperature(cityid):
177     result = OpenWeatherMap.query.filter_by(cityid=cityid).order_by(OpenWeatherMap.datetime.desc()).first()
178     return result.temp, result.datetime
179
180
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
184
185
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)
191
192
193 @app.route('/api/<version>/sensor/id/<sensor_id>')
194 def sensorid(version, sensor_id):
195     """Return all data for a specific sensor
196
197     URL parameters:
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
202     """
203     result = sensordata(sensor_id=sensor_id)
204     return jsonify(result)
205
206
207 @app.route('/api/<version>/sensor/type/<sensor_type>')
208 def sensortype(version, sensor_type):
209     """Return all data for a specific sensor type
210
211     URL parameters:
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
216     """
217     result = sensordata(sensor_type=sensor_type)
218     return jsonify(result)
219
220
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)
226
227
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)
233
234
235 @app.route("/")
236 def index():
237     airvalue, airtime     = currentairtemperature(cityid)
238     watervalue, watertime = currentwatertemperature(mainsensor)
239
240     return render_template(
241         'seepark_web.html',
242         apikey=apikey,
243         watervalue=watervalue,
244         watertime=watertime,
245         airvalue=airvalue,
246         airtime=airtime,
247     )