homekit/bin/web_api.py
2024-02-19 01:44:11 +03:00

216 lines
6.5 KiB
Python
Executable File

#!/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: web.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: web.Request):
message = "nothing here, keep lurking"
if is_development_mode():
message += ' (dev mode)'
return web.Response(text=message, content_type='text/plain')
async def GET_sensors_data(self, req: web.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: web.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: web.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: web.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: web.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: web.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: web.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: web.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()