diff options
author | Evgeny Zinoviev <me@ch1p.io> | 2023-09-27 00:54:57 +0300 |
---|---|---|
committer | Evgeny Zinoviev <me@ch1p.io> | 2023-09-27 00:54:57 +0300 |
commit | d3a295872c49defb55fc8e4e43e55550991e0927 (patch) | |
tree | b9dca15454f9027d5a9dad0d4443a20de04dbc5d /bin/web_api.py | |
parent | b7cbc2571c1870b4582ead45277d0aa7f961bec8 (diff) | |
parent | bdbb296697f55f4c3a07af43c9aaf7a9ea86f3d0 (diff) |
Merge branch 'master' of ch1p.io:homekit
Diffstat (limited to 'bin/web_api.py')
-rwxr-xr-x | bin/web_api.py | 215 |
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() |