summaryrefslogtreecommitdiff
path: root/bin/web_api.py
diff options
context:
space:
mode:
Diffstat (limited to 'bin/web_api.py')
-rwxr-xr-xbin/web_api.py215
1 files changed, 215 insertions, 0 deletions
diff --git a/bin/web_api.py b/bin/web_api.py
new file mode 100755
index 0000000..d221838
--- /dev/null
+++ b/bin/web_api.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+import asyncio
+import json
+import os
+import __py_include
+
+from datetime import datetime, timedelta
+
+from aiohttp import web
+from homekit import http
+from homekit.config import config, is_development_mode
+from homekit.database import BotsDatabase, SensorsDatabase, InverterDatabase
+from homekit.database.inverter_time_formats import *
+from homekit.api.types import TemperatureSensorLocation, SoundSensorLocation
+from homekit.media import SoundRecordStorage
+
+
+def strptime_auto(s: str) -> datetime:
+ e = None
+ for fmt in (FormatTime, FormatDate):
+ try:
+ return datetime.strptime(s, fmt)
+ except ValueError as _e:
+ e = _e
+ raise e
+
+
+class AuthError(Exception):
+ def __init__(self, message: str):
+ super().__init__()
+ self.message = message
+
+
+class WebAPIServer(http.HTTPServer):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.app.middlewares.append(self.validate_auth)
+
+ self.get('/', self.get_index)
+ self.get('/sensors/data/', self.GET_sensors_data)
+ self.get('/sound_sensors/hits/', self.GET_sound_sensors_hits)
+ self.post('/sound_sensors/hits/', self.POST_sound_sensors_hits)
+
+ self.post('/log/openwrt/', self.POST_openwrt_log)
+
+ self.get('/inverter/consumed_energy/', self.GET_consumed_energy)
+ self.get('/inverter/grid_consumed_energy/', self.GET_grid_consumed_energy)
+
+ self.get('/recordings/list/', self.GET_recordings_list)
+
+ @staticmethod
+ @web.middleware
+ async def validate_auth(req: http.Request, handler):
+ def get_token() -> str:
+ name = 'X-Token'
+ if name in req.headers:
+ return req.headers[name]
+
+ return req.query['token']
+
+ try:
+ token = get_token()
+ except KeyError:
+ raise AuthError('no token')
+
+ if token != config['api']['token']:
+ raise AuthError('invalid token')
+
+ return await handler(req)
+
+ @staticmethod
+ async def get_index(req: http.Request):
+ message = "nothing here, keep lurking"
+ if is_development_mode():
+ message += ' (dev mode)'
+ return http.Response(text=message, content_type='text/plain')
+
+ async def GET_sensors_data(self, req: http.Request):
+ try:
+ hours = int(req.query['hours'])
+ if hours < 1 or hours > 24:
+ raise ValueError('invalid hours value')
+ except KeyError:
+ hours = 1
+
+ sensor = TemperatureSensorLocation(int(req.query['sensor']))
+
+ dt_to = datetime.now()
+ dt_from = dt_to - timedelta(hours=hours)
+
+ db = SensorsDatabase()
+ data = db.get_temperature_recordings(sensor, (dt_from, dt_to))
+ return self.ok(data)
+
+ async def GET_sound_sensors_hits(self, req: http.Request):
+ location = SoundSensorLocation(int(req.query['location']))
+
+ after = int(req.query['after'])
+ kwargs = {}
+ if after is None:
+ last = int(req.query['last'])
+ if last is None:
+ raise ValueError('you must pass `after` or `last` params')
+ else:
+ if not 0 < last < 100:
+ raise ValueError('invalid last value: must be between 0 and 100')
+ kwargs['last'] = last
+ else:
+ kwargs['after'] = datetime.fromtimestamp(after)
+
+ data = BotsDatabase().get_sound_hits(location, **kwargs)
+ return self.ok(data)
+
+ async def POST_sound_sensors_hits(self, req: http.Request):
+ hits = []
+ data = await req.post()
+ for hit, count in json.loads(data['hits']):
+ if not hasattr(SoundSensorLocation, hit.upper()):
+ raise ValueError('invalid sensor location')
+ if count < 1:
+ raise ValueError(f'invalid count: {count}')
+ hits.append((SoundSensorLocation[hit.upper()], count))
+
+ BotsDatabase().add_sound_hits(hits, datetime.now())
+ return self.ok()
+
+ async def POST_openwrt_log(self, req: http.Request):
+ data = await req.post()
+
+ try:
+ logs = data['logs']
+ ap = int(data['ap'])
+ except KeyError:
+ logs = ''
+ ap = 0
+
+ # validate it
+ logs = json.loads(logs)
+ assert type(logs) is list, "invalid json data (list expected)"
+
+ lines = []
+ for line in logs:
+ assert type(line) is list, "invalid line type (list expected)"
+ assert len(line) == 2, f"expected 2 items in line, got {len(line)}"
+ assert type(line[0]) is int, "invalid line[0] type (int expected)"
+ assert type(line[1]) is str, "invalid line[1] type (str expected)"
+
+ lines.append((
+ datetime.fromtimestamp(line[0]),
+ line[1]
+ ))
+
+ BotsDatabase().add_openwrt_logs(lines, ap)
+ return self.ok()
+
+ async def GET_recordings_list(self, req: http.Request):
+ data = await req.post()
+
+ try:
+ extended = bool(int(data['extended']))
+ except KeyError:
+ extended = False
+
+ node = data['node']
+
+ root = os.path.join(config['recordings']['directory'], node)
+ if not os.path.isdir(root):
+ raise ValueError(f'invalid node {node}: no such directory')
+
+ storage = SoundRecordStorage(root)
+ files = storage.getfiles(as_objects=extended)
+ if extended:
+ files = list(map(lambda file: file.__dict__(), files))
+
+ return self.ok(files)
+
+ @staticmethod
+ def _get_inverter_from_to(req: http.Request):
+ s_from = req.query['from']
+ s_to = req.query['to']
+
+ dt_from = strptime_auto(s_from)
+
+ if s_to == 'now':
+ dt_to = datetime.now()
+ else:
+ dt_to = strptime_auto(s_to)
+
+ return dt_from, dt_to
+
+ async def GET_consumed_energy(self, req: http.Request):
+ dt_from, dt_to = self._get_inverter_from_to(req)
+ wh = InverterDatabase().get_consumed_energy(dt_from, dt_to)
+ return self.ok(wh)
+
+ async def GET_grid_consumed_energy(self, req: http.Request):
+ dt_from, dt_to = self._get_inverter_from_to(req)
+ wh = InverterDatabase().get_grid_consumed_energy(dt_from, dt_to)
+ return self.ok(wh)
+
+
+# start of the program
+# --------------------
+
+if __name__ == '__main__':
+ _app_name = 'web_api'
+ if is_development_mode():
+ _app_name += '_dev'
+ config.load_app(_app_name)
+
+ loop = asyncio.get_event_loop()
+
+ server = WebAPIServer(config.get_addr('server.listen'))
+ server.run()