216 lines
6.5 KiB
Python
Executable File
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()
|