summaryrefslogtreecommitdiff
path: root/include/py/homekit
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2024-02-19 01:44:02 +0300
committerEvgeny Zinoviev <me@ch1p.io>2024-02-19 01:44:11 +0300
commit3741f7cf78a288e967415ccb6736c888a21c211b (patch)
treea48d8331c9936d6c108de4d0f9179a089b1e56e6 /include/py/homekit
parentd79309e498cdc1358c81367ce2a93a5731e517d1 (diff)
web_kbn: almost completely ported lws to python
Diffstat (limited to 'include/py/homekit')
-rw-r--r--include/py/homekit/config/config.py2
-rw-r--r--include/py/homekit/http/__init__.py3
-rw-r--r--include/py/homekit/http/http.py132
-rw-r--r--include/py/homekit/media/node_server.py10
-rw-r--r--include/py/homekit/openwrt/__init__.py9
-rw-r--r--include/py/homekit/openwrt/config.py14
-rw-r--r--include/py/homekit/openwrt/openwrt.py90
-rw-r--r--include/py/homekit/soundsensor/server.py11
-rw-r--r--include/py/homekit/util.py5
9 files changed, 191 insertions, 85 deletions
diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py
index 3aa0e04..1eec97d 100644
--- a/include/py/homekit/config/config.py
+++ b/include/py/homekit/config/config.py
@@ -67,7 +67,7 @@ class BaseConfigUnit(ABC):
return self._data
cur = self._data
- pts = key.split('.')
+ pts = str(key).split('.')
for i in range(len(pts)):
k = pts[i]
if i < len(pts)-1:
diff --git a/include/py/homekit/http/__init__.py b/include/py/homekit/http/__init__.py
index d019e4c..f3721a4 100644
--- a/include/py/homekit/http/__init__.py
+++ b/include/py/homekit/http/__init__.py
@@ -1,2 +1 @@
-from .http import serve, ok, routes, HTTPServer, HTTPMethod
-from aiohttp.web import FileResponse, StreamResponse, Request, Response \ No newline at end of file
+from .http import serve, ajax_ok, HTTPMethod
diff --git a/include/py/homekit/http/http.py b/include/py/homekit/http/http.py
index 8819c46..a8c7d82 100644
--- a/include/py/homekit/http/http.py
+++ b/include/py/homekit/http/http.py
@@ -1,17 +1,46 @@
import logging
import asyncio
+import html
from enum import Enum
from aiohttp import web
-from aiohttp.web import Response, HTTPFound
+from aiohttp.web import HTTPFound
from aiohttp.web_exceptions import HTTPNotFound
-
from ..util import stringify, format_tb, Addr
-
_logger = logging.getLogger(__name__)
+def _render_error(error_type, error_message, traceback=None, code=500):
+ traceback_html = ''
+ if traceback:
+ traceback = '\n\n'.join(traceback)
+ traceback_html = f"""
+<div class="error_traceback">
+ <div class="error_title">Traceback</div>
+ <div class="error_traceback_content">{html.escape(traceback)}</div>
+</div>
+"""
+
+ buf = f"""
+<!doctype html>
+<html lang=en>
+<head>
+<title>Error: {html.escape(error_type)}</title>
+<meta http-equiv="content-type" content="text/html; charset=utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<link rel="stylesheet" type="text/css" href="/assets/error_page.css">
+</head>
+<body>
+<div class="error_title">{html.escape(error_type)}</div>
+<div class="error_message">{html.escape(error_message)}</div>
+{traceback_html}
+</body>
+</html>
+"""
+ return web.Response(text=buf, status=code, content_type='text/html')
+
+
@web.middleware
async def errors_handler_middleware(request, handler):
try:
@@ -19,97 +48,56 @@ async def errors_handler_middleware(request, handler):
return response
except HTTPNotFound:
- return web.json_response({'error': 'not found'}, status=404)
+ return _render_error(
+ error_type='Not Found',
+ error_message='The page you requested has not been found.',
+ code=404
+ )
except HTTPFound as exc:
raise exc
except Exception as exc:
_logger.exception(exc)
- data = {
- 'error': exc.__class__.__name__,
- 'message': exc.message if hasattr(exc, 'message') else str(exc)
- }
- tb = format_tb(exc)
- if tb:
- data['stacktrace'] = tb
+ return _render_error(
+ error_type=exc.__class__.__name__,
+ error_message=exc.message if hasattr(exc, 'message') else str(exc),
+ traceback=format_tb(exc)
+ )
- return web.json_response(data, status=500)
-
-def serve(addr: Addr, route_table: web.RouteTableDef, handle_signals: bool = True):
+def serve(addr: Addr, before_start=None, handle_signals=True, routes=None, event_loop=None):
app = web.Application()
- app.add_routes(route_table)
app.middlewares.append(errors_handler_middleware)
- host, port = addr
+ if routes is not None:
+ app.add_routes(routes)
- web.run_app(app,
- host=host,
- port=port,
- handle_signals=handle_signals)
+ if callable(before_start):
+ before_start(app)
+ if not event_loop:
+ event_loop = asyncio.get_event_loop()
-def routes() -> web.RouteTableDef:
- return web.RouteTableDef()
+ runner = web.AppRunner(app, handle_signals=handle_signals)
+ event_loop.run_until_complete(runner.setup())
+ host, port = addr
+ site = web.TCPSite(runner, host=host, port=port)
+ event_loop.run_until_complete(site.start())
-def ok(data=None):
+ _logger.info(f'Server started at http://{host}:{port}')
+
+ event_loop.run_forever()
+
+
+def ajax_ok(data=None):
if data is None:
data = 1
response = {'response': data}
return web.json_response(response, dumps=stringify)
-class HTTPServer:
- def __init__(self, addr: Addr, handle_errors=True):
- self.addr = addr
- self.app = web.Application()
- self.logger = logging.getLogger(self.__class__.__name__)
-
- if handle_errors:
- self.app.middlewares.append(errors_handler_middleware)
-
- def _add_route(self,
- method: str,
- path: str,
- handler: callable):
- self.app.router.add_routes([getattr(web, method)(path, handler)])
-
- def get(self, path, handler):
- self._add_route('get', path, handler)
-
- def post(self, path, handler):
- self._add_route('post', path, handler)
-
- def put(self, path, handler):
- self._add_route('put', path, handler)
-
- def delete(self, path, handler):
- self._add_route('delete', path, handler)
-
- def run(self, event_loop=None, handle_signals=True):
- if not event_loop:
- event_loop = asyncio.get_event_loop()
-
- runner = web.AppRunner(self.app, handle_signals=handle_signals)
- event_loop.run_until_complete(runner.setup())
-
- host, port = self.addr
- site = web.TCPSite(runner, host=host, port=port)
- event_loop.run_until_complete(site.start())
-
- self.logger.info(f'Server started at http://{host}:{port}')
-
- event_loop.run_forever()
-
- def ok(self, data=None):
- return ok(data)
-
- def plain(self, text: str):
- return Response(text=text, content_type='text/plain')
-
-
class HTTPMethod(Enum):
GET = 'GET'
POST = 'POST'
diff --git a/include/py/homekit/media/node_server.py b/include/py/homekit/media/node_server.py
index 5d0803c..229b9f7 100644
--- a/include/py/homekit/media/node_server.py
+++ b/include/py/homekit/media/node_server.py
@@ -33,12 +33,12 @@ class MediaNodeServer(http.HTTPServer):
raise ValueError(f'invalid duration: max duration is {max}')
record_id = self.recorder.record(duration)
- return http.ok({'id': record_id})
+ return http.ajax_ok({'id': record_id})
async def record_info(self, request: http.Request):
record_id = int(request.match_info['id'])
info = self.recorder.get_info(record_id)
- return http.ok(info.as_dict())
+ return http.ajax_ok(info.as_dict())
async def record_forget(self, request: http.Request):
record_id = int(request.match_info['id'])
@@ -47,7 +47,7 @@ class MediaNodeServer(http.HTTPServer):
assert info.status in (RecordStatus.FINISHED, RecordStatus.ERROR), f"can't forget: record status is {info.status}"
self.recorder.forget(record_id)
- return http.ok()
+ return http.ajax_ok()
async def record_download(self, request: http.Request):
record_id = int(request.match_info['id'])
@@ -64,7 +64,7 @@ class MediaNodeServer(http.HTTPServer):
if extended:
files = list(map(lambda file: file.__dict__(), files))
- return http.ok({
+ return http.ajax_ok({
'files': files
})
@@ -75,7 +75,7 @@ class MediaNodeServer(http.HTTPServer):
raise ValueError(f'file {file} not found')
self.storage.delete(file)
- return http.ok()
+ return http.ajax_ok()
async def storage_download(self, request):
file_id = request.query['file_id']
diff --git a/include/py/homekit/openwrt/__init__.py b/include/py/homekit/openwrt/__init__.py
new file mode 100644
index 0000000..b233b00
--- /dev/null
+++ b/include/py/homekit/openwrt/__init__.py
@@ -0,0 +1,9 @@
+from .config import OpenwrtConfig
+from .openwrt import (
+ ipset_list_all,
+ ipset_add,
+ ipset_del,
+ set_upstream,
+ get_default_route,
+ get_dhcp_leases
+)
diff --git a/include/py/homekit/openwrt/config.py b/include/py/homekit/openwrt/config.py
new file mode 100644
index 0000000..bd75d1c
--- /dev/null
+++ b/include/py/homekit/openwrt/config.py
@@ -0,0 +1,14 @@
+from typing import Optional
+
+from homekit.config import ConfigUnit
+
+
+class OpenwrtConfig(ConfigUnit):
+ NAME = 'openwrt'
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ 'ip': cls._addr_schema(only_ip=True, required=True),
+ 'command_id': {'type': 'string', 'required': True}
+ } \ No newline at end of file
diff --git a/include/py/homekit/openwrt/openwrt.py b/include/py/homekit/openwrt/openwrt.py
new file mode 100644
index 0000000..d5f949c
--- /dev/null
+++ b/include/py/homekit/openwrt/openwrt.py
@@ -0,0 +1,90 @@
+import requests
+import logging
+
+from datetime import datetime
+from collections import namedtuple
+from urllib.parse import quote_plus
+from .config import OpenwrtConfig
+from ..modem.config import ModemsConfig
+
+DHCPLease = namedtuple('DHCPLease', 'time, time_s, mac, ip, hostname')
+_config = OpenwrtConfig()
+_modems_config = ModemsConfig()
+_logger = logging.getLogger(__name__)
+
+
+def ipset_list_all() -> list:
+ args = ['ipset-list-all']
+ args += _modems_config.keys()
+ lines = _to_list(_call(args))
+ sets = {}
+ cur_set = None
+ for line in lines:
+ if line.startswith('>'):
+ cur_set = line[1:]
+ if cur_set not in sets:
+ sets[cur_set] = []
+ continue
+
+ if cur_set is None:
+ _logger.error('ipset_list_all: cur_set is not set')
+ continue
+
+ sets[cur_set].append(line)
+
+ return sets
+
+
+def ipset_add(set_name: str, ip: str):
+ return _call(['ipset-add', set_name, ip])
+
+
+def ipset_del(set_name: str, ip: str):
+ return _call(['ipset-del', set_name, ip])
+
+
+def set_upstream(ip: str):
+ return _call(['homekit-set-default-upstream', ip])
+
+
+def get_default_route():
+ return _call(['get-default-route'])
+
+
+def get_dhcp_leases() -> list[DHCPLease]:
+ return list(map(lambda item: _to_dhcp_lease(item), _to_list(_call(['dhcp-leases']))))
+
+
+def _call(arguments: list[str]) -> str:
+ url = _get_link(arguments)
+ r = requests.get(url)
+ r.raise_for_status()
+ return r.text.strip()
+
+
+def _get_link(arguments: list[str]) -> str:
+ url = f'http://{_config["ip"]}/cgi-bin/luci/command/{_config["command_id"]}'
+ if arguments:
+ url += '/'
+ url += '%20'.join(list(map(lambda arg: quote_plus(arg.replace('/', '_')), arguments)))
+ return url
+
+
+def _to_list(s: str) -> list:
+ return [] if s == '' else s.split('\n')
+
+
+def _to_dhcp_lease(s: str) -> DHCPLease:
+ words = s.split(' ')
+ time = int(words.pop(0))
+ mac = words.pop(0)
+ ip = words.pop(0)
+ words.pop()
+ hostname = (' '.join(words)).strip()
+ if not hostname or hostname == '*':
+ hostname = '?'
+ return DHCPLease(time=time,
+ time_s=datetime.fromtimestamp(time).strftime('%d %b, %H:%M:%S'),
+ mac=mac,
+ ip=ip,
+ hostname=hostname) \ No newline at end of file
diff --git a/include/py/homekit/soundsensor/server.py b/include/py/homekit/soundsensor/server.py
index a627390..d6320c1 100644
--- a/include/py/homekit/soundsensor/server.py
+++ b/include/py/homekit/soundsensor/server.py
@@ -3,6 +3,7 @@ import json
import logging
import threading
+from aiohttp import web
from ..database.sqlite import SQLiteBase
from ..config import config
from .. import http
@@ -108,21 +109,21 @@ class SoundSensorServer:
loop.run_forever()
def run_guard_server(self):
- routes = http.routes()
+ routes = web.RouteTableDef()
@routes.post('/guard/enable')
async def guard_enable(request):
self.set_recording(True)
- return http.ok()
+ return http.ajax_ok()
@routes.post('/guard/disable')
async def guard_disable(request):
self.set_recording(False)
- return http.ok()
+ return http.ajax_ok()
@routes.get('/guard/status')
async def guard_status(request):
- return http.ok({'enabled': self.is_recording_enabled()})
+ return http.ajax_ok({'enabled': self.is_recording_enabled()})
asyncio.set_event_loop(asyncio.new_event_loop()) # need to create new event loop in new thread
- http.serve(self.addr, routes, handle_signals=False) # handle_signals=True doesn't work in separate thread
+ http.serve(self.addr, handle_signals=False) # handle_signals=True doesn't work in separate thread
diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py
index 4410251..2d76968 100644
--- a/include/py/homekit/util.py
+++ b/include/py/homekit/util.py
@@ -105,6 +105,11 @@ class Addr:
yield self.host
yield self.port
+ def __eq__(self, other):
+ if isinstance(other, str):
+ return self.__str__() == other
+ return NotImplemented
+
# https://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks
def chunks(lst, n):