diff options
author | Evgeny Zinoviev <me@ch1p.io> | 2024-02-19 01:44:02 +0300 |
---|---|---|
committer | Evgeny Zinoviev <me@ch1p.io> | 2024-02-19 01:44:11 +0300 |
commit | 3741f7cf78a288e967415ccb6736c888a21c211b (patch) | |
tree | a48d8331c9936d6c108de4d0f9179a089b1e56e6 /include/py/homekit | |
parent | d79309e498cdc1358c81367ce2a93a5731e517d1 (diff) |
web_kbn: almost completely ported lws to python
Diffstat (limited to 'include/py/homekit')
-rw-r--r-- | include/py/homekit/config/config.py | 2 | ||||
-rw-r--r-- | include/py/homekit/http/__init__.py | 3 | ||||
-rw-r--r-- | include/py/homekit/http/http.py | 132 | ||||
-rw-r--r-- | include/py/homekit/media/node_server.py | 10 | ||||
-rw-r--r-- | include/py/homekit/openwrt/__init__.py | 9 | ||||
-rw-r--r-- | include/py/homekit/openwrt/config.py | 14 | ||||
-rw-r--r-- | include/py/homekit/openwrt/openwrt.py | 90 | ||||
-rw-r--r-- | include/py/homekit/soundsensor/server.py | 11 | ||||
-rw-r--r-- | include/py/homekit/util.py | 5 |
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): |