diff options
author | Evgeny Sorokin <me@ch1p.io> | 2024-01-16 02:05:00 +0300 |
---|---|---|
committer | Evgeny Sorokin <me@ch1p.io> | 2024-01-16 02:05:00 +0300 |
commit | da5db8bc280deab0e2081f39d2f32aabb2372afe (patch) | |
tree | 913c2f8c985b16b550d0ec58ba3ca469bf5a43fd | |
parent | 7058d0f5063dc9b065248d0a906cf874788caecf (diff) |
wip
-rw-r--r-- | bin/web_kbn.py | 90 | ||||
-rw-r--r-- | include/py/homekit/config/config.py | 3 | ||||
-rw-r--r-- | include/py/homekit/http/__init__.py | 4 | ||||
-rw-r--r-- | include/py/homekit/http/http.py | 6 | ||||
-rw-r--r-- | include/py/homekit/modem/__init__.py | 3 | ||||
-rw-r--r-- | include/py/homekit/modem/e3372.py | 253 | ||||
-rw-r--r-- | include/py/homekit/util.py | 21 | ||||
-rw-r--r-- | localwebsite/classes/E3372.php | 310 | ||||
-rw-r--r-- | localwebsite/handlers/ModemHandler.php | 85 | ||||
-rw-r--r-- | localwebsite/templates-web/modem_data.twig | 14 | ||||
-rw-r--r-- | localwebsite/templates-web/modem_status_page.twig | 19 | ||||
-rw-r--r-- | localwebsite/templates-web/modem_verbose_page.twig | 15 | ||||
-rw-r--r-- | localwebsite/templates-web/spinner.twig | 14 | ||||
-rwxr-xr-x | test/test_modems.py | 9 | ||||
-rw-r--r-- | web/kbn_assets/app.css | 2 | ||||
-rw-r--r-- | web/kbn_assets/app.js | 38 | ||||
-rw-r--r-- | web/kbn_templates/base.j2 | 6 | ||||
-rw-r--r-- | web/kbn_templates/modem_data.j2 | 13 | ||||
-rw-r--r-- | web/kbn_templates/modem_verbose.j2 | 18 | ||||
-rw-r--r-- | web/kbn_templates/modems.j2 | 4 | ||||
-rw-r--r-- | web/kbn_templates/signal_level.j2 (renamed from localwebsite/templates-web/signal_level.twig) | 2 |
21 files changed, 433 insertions, 496 deletions
diff --git a/bin/web_kbn.py b/bin/web_kbn.py index 8b4ca6f..75437f1 100644 --- a/bin/web_kbn.py +++ b/bin/web_kbn.py @@ -2,16 +2,18 @@ import asyncio import jinja2 import aiohttp_jinja2 +import json import os +import re import __py_include from io import StringIO -from typing import Optional +from typing import Optional, Union from homekit.config import config, AppConfigUnit -from homekit.util import homekit_path +from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string from aiohttp import web from homekit import http -from homekit.modem import ModemsConfig +from homekit.modem import ModemsConfig, E3372, MacroNetWorkType class WebKbnConfig(AppConfigUnit): @@ -49,7 +51,7 @@ def get_css_link(file, version) -> str: def get_head_static() -> str: buf = StringIO() for file in STATIC_FILES: - v = 1 + v = 2 try: q_ind = file.index('?') v = file[q_ind+1:] @@ -64,19 +66,52 @@ def get_head_static() -> str: return buf.getvalue() +def get_modem_data(modem_cfg: dict, get_raw=False) -> Union[dict, tuple]: + cl = E3372(modem_cfg['ip'], legacy_token_auth=modem_cfg['legacy_auth']) + + signal = cl.device_signal + status = cl.monitoring_status + traffic = cl.traffic_stats + + if get_raw: + device_info = cl.device_information + dialup_conn = cl.dialup_connection + return signal, status, traffic, device_info, dialup_conn + else: + network_type_label = re.sub('^MACRO_NET_WORK_TYPE(_EX)?_', '', MacroNetWorkType(int(status['CurrentNetworkType'])).name) + return { + 'type': network_type_label, + 'level': int(status['SignalIcon']) if 'SignalIcon' in status else 0, + 'rssi': signal['rssi'], + 'sinr': signal['sinr'], + 'connected_time': seconds_to_human_readable_string(int(traffic['CurrentConnectTime'])), + 'downloaded': filesize_fmt(int(traffic['CurrentDownload'])), + 'uploaded': filesize_fmt(int(traffic['CurrentUpload'])) + } + + class WebSite(http.HTTPServer): + _modems_config: ModemsConfig + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self._modems_config = ModemsConfig() + aiohttp_jinja2.setup( self.app, - loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')) + loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')), + autoescape=jinja2.select_autoescape(['html', 'xml']), ) + env = aiohttp_jinja2.get_env(self.app) + env.filters['tojson'] = lambda obj: json.dumps(obj, separators=(',', ':')) self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets')) self.get('/main.cgi', self.get_index) self.get('/modems.cgi', self.get_modems) + self.get('/modems/info.ajx', self.get_modems_ajax) + self.get('/modems/verbose.cgi', self.get_modems_verbose) async def render_page(self, req: http.Request, @@ -99,19 +134,50 @@ class WebSite(http.HTTPServer): title="Home web site") async def get_modems(self, req: http.Request): - mc = ModemsConfig() - print(mc) return await self.render_page(req, 'modems', title='Состояние модемов', - context=dict(modems=ModemsConfig())) + context=dict(modems=self._modems_config)) + + async def get_modems_ajax(self, req: http.Request): + modem = req.query.get('id', None) + if modem not in self._modems_config.getkeys(): + raise ValueError('invalid modem id') + + modem_cfg = self._modems_config.get(modem) + loop = asyncio.get_event_loop() + modem_data = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg)) + + html = aiohttp_jinja2.render_string('modem_data.j2', req, context=dict( + modem_data=modem_data, + modem=modem + )) + + return self.ok({'html': html}) + + async def get_modems_verbose(self, req: http.Request): + modem = req.query.get('id', None) + if modem not in self._modems_config.getkeys(): + raise ValueError('invalid modem id') + + modem_cfg = self._modems_config.get(modem) + loop = asyncio.get_event_loop() + signal, status, traffic, device, dialup_conn = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg, True)) + data = [ + ['Signal', signal], + ['Connection', status], + ['Traffic', traffic], + ['Device info', device], + ['Dialup connection', dialup_conn] + ] + + modem_name = self._modems_config.getfullname(modem) + return await self.render_page(req, 'modem_verbose', + title=f'Подробная информация о модеме "{modem_name}"', + context=dict(data=data, modem_name=modem_name)) if __name__ == '__main__': config.load_app(WebKbnConfig) - loop = asyncio.get_event_loop() - # print(config.app_config) - - print(config.app_config['listen_addr'].host) server = WebSite(config.app_config['listen_addr']) server.run() diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index abdedad..eb2ad82 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -78,6 +78,9 @@ class BaseConfigUnit(ABC): raise KeyError(f'option {key} not found') + def getkeys(self): + return list(self._data.keys()) + class ConfigUnit(BaseConfigUnit): NAME = 'dumb' diff --git a/include/py/homekit/http/__init__.py b/include/py/homekit/http/__init__.py index 6030e95..d019e4c 100644 --- a/include/py/homekit/http/__init__.py +++ b/include/py/homekit/http/__init__.py @@ -1,2 +1,2 @@ -from .http import serve, ok, routes, HTTPServer -from aiohttp.web import FileResponse, StreamResponse, Request, Response +from .http import serve, ok, routes, HTTPServer, HTTPMethod +from aiohttp.web import FileResponse, StreamResponse, Request, Response
\ No newline at end of file diff --git a/include/py/homekit/http/http.py b/include/py/homekit/http/http.py index 3e70751..9b76d9a 100644 --- a/include/py/homekit/http/http.py +++ b/include/py/homekit/http/http.py @@ -1,6 +1,7 @@ import logging import asyncio +from enum import Enum from aiohttp import web from aiohttp.web import Response from aiohttp.web_exceptions import HTTPNotFound @@ -104,3 +105,8 @@ class HTTPServer: 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/modem/__init__.py b/include/py/homekit/modem/__init__.py index 20e75b7..ea0930e 100644 --- a/include/py/homekit/modem/__init__.py +++ b/include/py/homekit/modem/__init__.py @@ -1 +1,2 @@ -from .config import ModemsConfig
\ No newline at end of file +from .config import ModemsConfig +from .e3372 import E3372, MacroNetWorkType diff --git a/include/py/homekit/modem/e3372.py b/include/py/homekit/modem/e3372.py new file mode 100644 index 0000000..f68db5a --- /dev/null +++ b/include/py/homekit/modem/e3372.py @@ -0,0 +1,253 @@ +import requests +import xml.etree.ElementTree as ElementTree + +from ..util import Addr +from enum import Enum +from ..http import HTTPMethod +from typing import Union + + +class Error(Enum): + ERROR_SYSTEM_NO_SUPPORT = 100002 + ERROR_SYSTEM_NO_RIGHTS = 100003 + ERROR_SYSTEM_BUSY = 100004 + ERROR_LOGIN_USERNAME_WRONG = 108001 + ERROR_LOGIN_PASSWORD_WRONG = 108002 + ERROR_LOGIN_ALREADY_LOGIN = 108003 + ERROR_LOGIN_USERNAME_PWD_WRONG = 108006 + ERROR_LOGIN_USERNAME_PWD_ORERRUN = 108007 + ERROR_LOGIN_TOUCH_ALREADY_LOGIN = 108009 + ERROR_VOICE_BUSY = 120001 + ERROR_WRONG_TOKEN = 125001 + ERROR_WRONG_SESSION = 125002 + ERROR_WRONG_SESSION_TOKEN = 125003 + + +class WifiStatus(Enum): + WIFI_CONNECTING = '900' + WIFI_CONNECTED = '901' + WIFI_DISCONNECTED = '902' + WIFI_DISCONNECTING = '903' + + +class Cradle(Enum): + CRADLE_CONNECTING = '900' + CRADLE_CONNECTED = '901' + CRADLE_DISCONNECTED = '902' + CRADLE_DISCONNECTING = '903' + CRADLE_CONNECTFAILED = '904' + CRADLE_CONNECTSTATUSNULL = '905' + CRANDLE_CONNECTSTATUSERRO = '906' + + +class MacroEVDOLevel(Enum): + MACRO_EVDO_LEVEL_ZERO = '0' + MACRO_EVDO_LEVEL_ONE = '1' + MACRO_EVDO_LEVEL_TWO = '2' + MACRO_EVDO_LEVEL_THREE = '3' + MACRO_EVDO_LEVEL_FOUR = '4' + MACRO_EVDO_LEVEL_FIVE = '5' + + +class MacroNetWorkType(Enum): + MACRO_NET_WORK_TYPE_NOSERVICE = 0 + MACRO_NET_WORK_TYPE_GSM = 1 + MACRO_NET_WORK_TYPE_GPRS = 2 + MACRO_NET_WORK_TYPE_EDGE = 3 + MACRO_NET_WORK_TYPE_WCDMA = 4 + MACRO_NET_WORK_TYPE_HSDPA = 5 + MACRO_NET_WORK_TYPE_HSUPA = 6 + MACRO_NET_WORK_TYPE_HSPA = 7 + MACRO_NET_WORK_TYPE_TDSCDMA = 8 + MACRO_NET_WORK_TYPE_HSPA_PLUS = 9 + MACRO_NET_WORK_TYPE_EVDO_REV_0 = 10 + MACRO_NET_WORK_TYPE_EVDO_REV_A = 11 + MACRO_NET_WORK_TYPE_EVDO_REV_B = 12 + MACRO_NET_WORK_TYPE_1xRTT = 13 + MACRO_NET_WORK_TYPE_UMB = 14 + MACRO_NET_WORK_TYPE_1xEVDV = 15 + MACRO_NET_WORK_TYPE_3xRTT = 16 + MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM = 17 + MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO = 18 + MACRO_NET_WORK_TYPE_LTE = 19 + MACRO_NET_WORK_TYPE_EX_NOSERVICE = 0 + MACRO_NET_WORK_TYPE_EX_GSM = 1 + MACRO_NET_WORK_TYPE_EX_GPRS = 2 + MACRO_NET_WORK_TYPE_EX_EDGE = 3 + MACRO_NET_WORK_TYPE_EX_IS95A = 21 + MACRO_NET_WORK_TYPE_EX_IS95B = 22 + MACRO_NET_WORK_TYPE_EX_CDMA_1x = 23 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_0 = 24 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_A = 25 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_B = 26 + MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x = 27 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0 = 28 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A = 29 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B = 30 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0 = 31 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A = 32 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B = 33 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0 = 34 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A = 35 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B = 36 + MACRO_NET_WORK_TYPE_EX_WCDMA = 41 + MACRO_NET_WORK_TYPE_EX_HSDPA = 42 + MACRO_NET_WORK_TYPE_EX_HSUPA = 43 + MACRO_NET_WORK_TYPE_EX_HSPA = 44 + MACRO_NET_WORK_TYPE_EX_HSPA_PLUS = 45 + MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS = 46 + MACRO_NET_WORK_TYPE_EX_TD_SCDMA = 61 + MACRO_NET_WORK_TYPE_EX_TD_HSDPA = 62 + MACRO_NET_WORK_TYPE_EX_TD_HSUPA = 63 + MACRO_NET_WORK_TYPE_EX_TD_HSPA = 64 + MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS = 65 + MACRO_NET_WORK_TYPE_EX_802_16E = 81 + MACRO_NET_WORK_TYPE_EX_LTE = 101 + + +def post_data_to_xml(data: dict, depth: int = 1) -> str: + if depth == 1: + return '<?xml version: "1.0" encoding="UTF-8"?>'+post_data_to_xml({'request': data}, depth+1) + + items = [] + for k, v in data.items(): + if isinstance(v, dict): + v = post_data_to_xml(v, depth+1) + elif isinstance(v, list): + raise TypeError('list type is unsupported here') + items.append(f'<{k}>{v}</{k}>') + + return ''.join(items) + + +class E3372: + _addr: Addr + _need_auth: bool + _legacy_token_auth: bool + _get_raw_data: bool + _headers: dict[str, str] + _authorized: bool + + def __init__(self, + addr: Addr, + need_auth: bool = True, + legacy_token_auth: bool = False, + get_raw_data: bool = False): + self._addr = addr + self._need_auth = need_auth + self._legacy_token_auth = legacy_token_auth + self._get_raw_data = get_raw_data + self._authorized = False + self._headers = {} + + @property + def device_information(self): + self.auth() + return self.request('device/information') + + @property + def device_signal(self): + self.auth() + return self.request('device/signal') + + @property + def monitoring_status(self): + self.auth() + return self.request('monitoring/status') + + @property + def notifications(self): + self.auth() + return self.request('monitoring/check-notifications') + + @property + def dialup_connection(self): + self.auth() + return self.request('dialup/connection') + + @property + def traffic_stats(self): + self.auth() + return self.request('monitoring/traffic-statistics') + + @property + def sms_count(self): + self.auth() + return self.request('sms/sms-count') + + def sms_send(self, phone: str, text: str): + self.auth() + return self.request('sms/send-sms', HTTPMethod.POST, { + 'Index': -1, + 'Phones': { + 'Phone': phone + }, + 'Sca': '', + 'Content': text, + 'Length': -1, + 'Reserved': 1, + 'Date': -1 + }) + + def sms_list(self, page: int = 1, count: int = 20, outbox: bool = False): + self.auth() + xml = self.request('sms/sms-list', HTTPMethod.POST, { + 'PageIndex': page, + 'ReadCount': count, + 'BoxType': 1 if not outbox else 2, + 'SortType': 0, + 'Ascending': 0, + 'UnreadPreferred': 1 if not outbox else 0 + }, return_body=True) + + root = ElementTree.fromstring(xml) + messages = [] + for message_elem in root.find('Messages').findall('Message'): + message_dict = {child.tag: child.text for child in message_elem} + messages.append(message_dict) + return messages + + def auth(self): + if self._authorized: + return + + if not self._legacy_token_auth: + data = self.request('webserver/SesTokInfo') + self._headers = { + 'Cookie': data['SesInfo'], + '__RequestVerificationToken': data['TokInfo'], + 'Content-Type': 'text/xml' + } + else: + data = self.request('webserver/token') + self._headers = { + '__RequestVerificationToken': data['token'], + 'Content-Type': 'text/xml' + } + + self._authorized = True + + def request(self, + method: str, + http_method: HTTPMethod = HTTPMethod.GET, + data: dict = {}, + return_body: bool = False) -> Union[str, dict]: + url = f'http://{self._addr}/api/{method}' + if http_method == HTTPMethod.POST: + data = post_data_to_xml(data) + f = requests.post + else: + data = None + f = requests.get + r = f(url, data=data, headers=self._headers) + r.raise_for_status() + r.encoding = 'utf-8' + + if return_body: + return r.text + + root = ElementTree.fromstring(r.text) + data_dict = {} + for elem in root: + data_dict[elem.tag] = elem.text + return data_dict diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py index 3c73440..f267488 100644 --- a/include/py/homekit/util.py +++ b/include/py/homekit/util.py @@ -12,7 +12,7 @@ import re import os from enum import Enum -from datetime import datetime +from datetime import datetime, timedelta from typing import Optional, List from zlib import adler32 @@ -255,6 +255,25 @@ def filesize_fmt(num, suffix="B") -> str: return f"{num:.1f} Yi{suffix}" +def seconds_to_human_readable_string(seconds: int) -> str: + duration = timedelta(seconds=seconds) + days, remainder = divmod(duration.total_seconds(), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + + parts = [] + if days > 0: + parts.append(f"{int(days)} day{'s' if days > 1 else ''}") + if hours > 0: + parts.append(f"{int(hours)} hour{'s' if hours > 1 else ''}") + if minutes > 0: + parts.append(f"{int(minutes)} minute{'s' if minutes > 1 else ''}") + if seconds > 0: + parts.append(f"{int(seconds)} second{'s' if seconds > 1 else ''}") + + return ' '.join(parts) + + class HashableEnum(Enum): def hash(self) -> int: return adler32(self.name.encode()) diff --git a/localwebsite/classes/E3372.php b/localwebsite/classes/E3372.php deleted file mode 100644 index a3ce80c..0000000 --- a/localwebsite/classes/E3372.php +++ /dev/null @@ -1,310 +0,0 @@ -<?php - -class E3372 -{ - - const WIFI_CONNECTING = '900'; - const WIFI_CONNECTED = '901'; - const WIFI_DISCONNECTED = '902'; - const WIFI_DISCONNECTING = '903'; - - const CRADLE_CONNECTING = '900'; - const CRADLE_CONNECTED = '901'; - const CRADLE_DISCONNECTED = '902'; - const CRADLE_DISCONNECTING = '903'; - const CRADLE_CONNECTFAILED = '904'; - const CRADLE_CONNECTSTATUSNULL = '905'; - const CRANDLE_CONNECTSTATUSERRO = '906'; - - const MACRO_EVDO_LEVEL_ZERO = '0'; - const MACRO_EVDO_LEVEL_ONE = '1'; - const MACRO_EVDO_LEVEL_TWO = '2'; - const MACRO_EVDO_LEVEL_THREE = '3'; - const MACRO_EVDO_LEVEL_FOUR = '4'; - const MACRO_EVDO_LEVEL_FIVE = '5'; - - // CurrentNetworkType - const MACRO_NET_WORK_TYPE_NOSERVICE = 0; - const MACRO_NET_WORK_TYPE_GSM = 1; - const MACRO_NET_WORK_TYPE_GPRS = 2; - const MACRO_NET_WORK_TYPE_EDGE = 3; - const MACRO_NET_WORK_TYPE_WCDMA = 4; - const MACRO_NET_WORK_TYPE_HSDPA = 5; - const MACRO_NET_WORK_TYPE_HSUPA = 6; - const MACRO_NET_WORK_TYPE_HSPA = 7; - const MACRO_NET_WORK_TYPE_TDSCDMA = 8; - const MACRO_NET_WORK_TYPE_HSPA_PLUS = 9; - const MACRO_NET_WORK_TYPE_EVDO_REV_0 = 10; - const MACRO_NET_WORK_TYPE_EVDO_REV_A = 11; - const MACRO_NET_WORK_TYPE_EVDO_REV_B = 12; - const MACRO_NET_WORK_TYPE_1xRTT = 13; - const MACRO_NET_WORK_TYPE_UMB = 14; - const MACRO_NET_WORK_TYPE_1xEVDV = 15; - const MACRO_NET_WORK_TYPE_3xRTT = 16; - const MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM = 17; - const MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO = 18; - const MACRO_NET_WORK_TYPE_LTE = 19; - const MACRO_NET_WORK_TYPE_EX_NOSERVICE = 0; - const MACRO_NET_WORK_TYPE_EX_GSM = 1; - const MACRO_NET_WORK_TYPE_EX_GPRS = 2; - const MACRO_NET_WORK_TYPE_EX_EDGE = 3; - const MACRO_NET_WORK_TYPE_EX_IS95A = 21; - const MACRO_NET_WORK_TYPE_EX_IS95B = 22; - const MACRO_NET_WORK_TYPE_EX_CDMA_1x = 23; - const MACRO_NET_WORK_TYPE_EX_EVDO_REV_0 = 24; - const MACRO_NET_WORK_TYPE_EX_EVDO_REV_A = 25; - const MACRO_NET_WORK_TYPE_EX_EVDO_REV_B = 26; - const MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x = 27; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0 = 28; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A = 29; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B = 30; - const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0 = 31; - const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A = 32; - const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B = 33; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0 = 34; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A = 35; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B = 36; - const MACRO_NET_WORK_TYPE_EX_WCDMA = 41; - const MACRO_NET_WORK_TYPE_EX_HSDPA = 42; - const MACRO_NET_WORK_TYPE_EX_HSUPA = 43; - const MACRO_NET_WORK_TYPE_EX_HSPA = 44; - const MACRO_NET_WORK_TYPE_EX_HSPA_PLUS = 45; - const MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS = 46; - const MACRO_NET_WORK_TYPE_EX_TD_SCDMA = 61; - const MACRO_NET_WORK_TYPE_EX_TD_HSDPA = 62; - const MACRO_NET_WORK_TYPE_EX_TD_HSUPA = 63; - const MACRO_NET_WORK_TYPE_EX_TD_HSPA = 64; - const MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS = 65; - const MACRO_NET_WORK_TYPE_EX_802_16E = 81; - const MACRO_NET_WORK_TYPE_EX_LTE = 101; - - - const ERROR_SYSTEM_NO_SUPPORT = 100002; - const ERROR_SYSTEM_NO_RIGHTS = 100003; - const ERROR_SYSTEM_BUSY = 100004; - const ERROR_LOGIN_USERNAME_WRONG = 108001; - const ERROR_LOGIN_PASSWORD_WRONG = 108002; - const ERROR_LOGIN_ALREADY_LOGIN = 108003; - const ERROR_LOGIN_USERNAME_PWD_WRONG = 108006; - const ERROR_LOGIN_USERNAME_PWD_ORERRUN = 108007; - const ERROR_LOGIN_TOUCH_ALREADY_LOGIN = 108009; - const ERROR_VOICE_BUSY = 120001; - const ERROR_WRONG_TOKEN = 125001; - const ERROR_WRONG_SESSION = 125002; - const ERROR_WRONG_SESSION_TOKEN = 125003; - - private string $host; - private array $headers = []; - private bool $authorized = false; - private bool $useLegacyTokenAuth = false; - - public function __construct(string $host, bool $legacy_token_auth = false) { - $this->host = $host; - $this->useLegacyTokenAuth = $legacy_token_auth; - } - - public function auth() { - if ($this->authorized) - return; - - if (!$this->useLegacyTokenAuth) { - $data = $this->request('webserver/SesTokInfo'); - $this->headers = [ - 'Cookie: '.$data['SesInfo'], - '__RequestVerificationToken: '.$data['TokInfo'], - 'Content-Type: text/xml' - ]; - } else { - $data = $this->request('webserver/token'); - $this->headers = [ - '__RequestVerificationToken: '.$data['token'], - 'Content-Type: text/xml' - ]; - } - $this->authorized = true; - } - - public function getDeviceInformation() { - $this->auth(); - return $this->request('device/information'); - } - - public function getDeviceSignal() { - $this->auth(); - return $this->request('device/signal'); - } - - public function getMonitoringStatus() { - $this->auth(); - return $this->request('monitoring/status'); - } - - public function getNotifications() { - $this->auth(); - return $this->request('monitoring/check-notifications'); - } - - public function getDialupConnection() { - $this->auth(); - return $this->request('dialup/connection'); - } - - public function getTrafficStats() { - $this->auth(); - return $this->request('monitoring/traffic-statistics'); - } - - public function getSMSCount() { - $this->auth(); - return $this->request('sms/sms-count'); - } - - public function sendSMS(string $phone, string $text) { - $this->auth(); - return $this->request('sms/send-sms', 'POST', [ - 'Index' => -1, - 'Phones' => [ - 'Phone' => $phone - ], - 'Sca' => '', - 'Content' => $text, - 'Length' => -1, - 'Reserved' => 1, - 'Date' => -1 - ]); - } - - public function getSMSList(int $page = 1, int $count = 20, bool $outbox = false) { - $this->auth(); - $xml = $this->request('sms/sms-list', 'POST', [ - 'PageIndex' => $page, - 'ReadCount' => $count, - 'BoxType' => !$outbox ? 1 : 2, - 'SortType' => 0, - 'Ascending' => 0, - 'UnreadPreferred' => !$outbox ? 1 : 0 - ], true); - $xml = simplexml_load_string($xml); - - $messages = []; - foreach ($xml->Messages->Message as $message) { - $dt = DateTime::createFromFormat("Y-m-d H:i:s", (string)$message->Date); - $messages[] = [ - 'date' => (string)$message->Date, - 'timestamp' => $dt->getTimestamp(), - 'phone' => (string)$message->Phone, - 'content' => (string)$message->Content - ]; - } - return $messages; - } - - private function xmlToAssoc(string $xml): array { - $xml = new SimpleXMLElement($xml); - $data = []; - foreach ($xml as $name => $value) { - $data[$name] = (string)$value; - } - return $data; - } - - private function request(string $method, string $http_method = 'GET', array $data = [], bool $return_body = false) { - $ch = curl_init(); - $url = 'http://'.$this->host.'/api/'.$method; - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - if (!empty($this->headers)) - curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers); - if ($http_method == 'POST') { - curl_setopt($ch, CURLOPT_POST, true); - - $post_data = $this->postDataToXML($data); - // debugLog('post_data:', $post_data); - - if (!empty($data)) - curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); - } - $body = curl_exec($ch); - - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($code != 200) - throw new Exception('e3372 host returned code '.$code); - - curl_close($ch); - return $return_body ? $body : $this->xmlToAssoc($body); - } - - private function postDataToXML(array $data, int $depth = 1): string { - if ($depth == 1) - return '<?xml version: "1.0" encoding="UTF-8"?>'.$this->postDataToXML(['request' => $data], $depth+1); - - $items = []; - foreach ($data as $key => $value) { - if (is_array($value)) - $value = $this->postDataToXML($value, $depth+1); - $items[] = "<{$key}>{$value}</{$key}>"; - } - - return implode('', $items); - } - - public static function getNetworkTypeLabel($type): string { - switch ((int)$type) { - case self::MACRO_NET_WORK_TYPE_NOSERVICE: return 'NOSERVICE'; - case self::MACRO_NET_WORK_TYPE_GSM: return 'GSM'; - case self::MACRO_NET_WORK_TYPE_GPRS: return 'GPRS'; - case self::MACRO_NET_WORK_TYPE_EDGE: return 'EDGE'; - case self::MACRO_NET_WORK_TYPE_WCDMA: return 'WCDMA'; - case self::MACRO_NET_WORK_TYPE_HSDPA: return 'HSDPA'; - case self::MACRO_NET_WORK_TYPE_HSUPA: return 'HSUPA'; - case self::MACRO_NET_WORK_TYPE_HSPA: return 'HSPA'; - case self::MACRO_NET_WORK_TYPE_TDSCDMA: return 'TDSCDMA'; - case self::MACRO_NET_WORK_TYPE_HSPA_PLUS: return 'HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EVDO_REV_0: return 'EVDO_REV_0'; - case self::MACRO_NET_WORK_TYPE_EVDO_REV_A: return 'EVDO_REV_A'; - case self::MACRO_NET_WORK_TYPE_EVDO_REV_B: return 'EVDO_REV_B'; - case self::MACRO_NET_WORK_TYPE_1xRTT: return '1xRTT'; - case self::MACRO_NET_WORK_TYPE_UMB: return 'UMB'; - case self::MACRO_NET_WORK_TYPE_1xEVDV: return '1xEVDV'; - case self::MACRO_NET_WORK_TYPE_3xRTT: return '3xRTT'; - case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM: return 'HSPA_PLUS_64QAM'; - case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO: return 'HSPA_PLUS_MIMO'; - case self::MACRO_NET_WORK_TYPE_LTE: return 'LTE'; - case self::MACRO_NET_WORK_TYPE_EX_NOSERVICE: return 'NOSERVICE'; - case self::MACRO_NET_WORK_TYPE_EX_GSM: return 'GSM'; - case self::MACRO_NET_WORK_TYPE_EX_GPRS: return 'GPRS'; - case self::MACRO_NET_WORK_TYPE_EX_EDGE: return 'EDGE'; - case self::MACRO_NET_WORK_TYPE_EX_IS95A: return 'IS95A'; - case self::MACRO_NET_WORK_TYPE_EX_IS95B: return 'IS95B'; - case self::MACRO_NET_WORK_TYPE_EX_CDMA_1x: return 'CDMA_1x'; - case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_0: return 'EVDO_REV_0'; - case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_A: return 'EVDO_REV_A'; - case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_B: return 'EVDO_REV_B'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x: return 'HYBRID_CDMA_1x'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0: return 'HYBRID_EVDO_REV_0'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A: return 'HYBRID_EVDO_REV_A'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B: return 'HYBRID_EVDO_REV_B'; - case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0: return 'EHRPD_REL_0'; - case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A: return 'EHRPD_REL_A'; - case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B: return 'EHRPD_REL_B'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0: return 'HYBRID_EHRPD_REL_0'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A: return 'HYBRID_EHRPD_REL_A'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B: return 'HYBRID_EHRPD_REL_B'; - case self::MACRO_NET_WORK_TYPE_EX_WCDMA: return 'WCDMA'; - case self::MACRO_NET_WORK_TYPE_EX_HSDPA: return 'HSDPA'; - case self::MACRO_NET_WORK_TYPE_EX_HSUPA: return 'HSUPA'; - case self::MACRO_NET_WORK_TYPE_EX_HSPA: return 'HSPA'; - case self::MACRO_NET_WORK_TYPE_EX_HSPA_PLUS: return 'HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS: return 'DC_HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EX_TD_SCDMA: return 'TD_SCDMA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSDPA: return 'TD_HSDPA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSUPA: return 'TD_HSUPA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA: return 'TD_HSPA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS: return 'TD_HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EX_802_16E: return '802_16E'; - case self::MACRO_NET_WORK_TYPE_EX_LTE: return 'LTE'; - default: return '?'; - } - } - -} diff --git a/localwebsite/handlers/ModemHandler.php b/localwebsite/handlers/ModemHandler.php index 6743fe9..8179620 100644 --- a/localwebsite/handlers/ModemHandler.php +++ b/localwebsite/handlers/ModemHandler.php @@ -7,65 +7,6 @@ use libphonenumber\PhoneNumberUtil; class ModemHandler extends RequestHandler { - public function GET_status_page() { - global $config; - - $this->tpl->set([ - 'modems' => $config['modems'], - 'js_modems' => array_keys($config['modems']), - ]); - - $this->tpl->set_title('Состояние модемов'); - $this->tpl->render_page('modem_status_page.twig'); - } - - public function GET_status_get_ajax() { - global $config; - list($id) = $this->input('id'); - if (!isset($config['modems'][$id])) - ajax_error('invalid modem id: '.$id); - - $modem_data = self::getModemData( - $config['modems'][$id]['ip'], - $config['modems'][$id]['legacy_token_auth']); - - ajax_ok([ - 'html' => $this->tpl->render('modem_data.twig', [ - 'loading' => false, - 'modem' => $id, - 'modem_data' => $modem_data - ]) - ]); - } - - public function GET_verbose_page() { - global $config; - - list($modem) = $this->input('modem'); - if (!$modem) - $modem = array_key_first($config['modems']); - - list($signal, $status, $traffic, $device, $dialup_conn) = self::getModemData( - $config['modems'][$modem]['ip'], - $config['modems'][$modem]['legacy_token_auth'], - true); - - $data = [ - ['Signal', $signal], - ['Connection', $status], - ['Traffic', $traffic], - ['Device info', $device], - ['Dialup connection', $dialup_conn] - ]; - $this->tpl->set([ - 'data' => $data, - 'modem_name' => $config['modems'][$modem]['label'], - ]); - $this->tpl->set_title('Подробная информация о модеме '.$modem); - $this->tpl->render_page('modem_verbose_page.twig'); - } - - public function GET_routing_smallhome_page() { global $config; @@ -233,32 +174,6 @@ class ModemHandler extends RequestHandler $go_back(); } - protected static function getModemData(string $ip, - bool $need_auth = true, - bool $get_raw_data = false): array { - $modem = new E3372($ip, $need_auth); - - $signal = $modem->getDeviceSignal(); - $status = $modem->getMonitoringStatus(); - $traffic = $modem->getTrafficStats(); - - if ($get_raw_data) { - $device_info = $modem->getDeviceInformation(); - $dialup_conn = $modem->getDialupConnection(); - return [$signal, $status, $traffic, $device_info, $dialup_conn]; - } else { - return [ - 'type' => e3372::getNetworkTypeLabel($status['CurrentNetworkType']), - 'level' => $status['SignalIcon'] ?? 0, - 'rssi' => $signal['rssi'], - 'sinr' => $signal['sinr'], - 'connected_time' => secondsToTime($traffic['CurrentConnectTime']), - 'downloaded' => bytesToUnitsLabel(gmp_init($traffic['CurrentDownload'])), - 'uploaded' => bytesToUnitsLabel(gmp_init($traffic['CurrentUpload'])), - ]; - } - } - protected static function getCurrentUpstream() { global $config; diff --git a/localwebsite/templates-web/modem_data.twig b/localwebsite/templates-web/modem_data.twig deleted file mode 100644 index a2c00e5..0000000 --- a/localwebsite/templates-web/modem_data.twig +++ /dev/null @@ -1,14 +0,0 @@ -{% if not loading %} - <span class="text-secondary">Сигнал:</span> {% include 'signal_level.twig' with {'level': modem_data.level} %}<br> - <span class="text-secondary">Тип сети:</span> <b>{{ modem_data.type }}</b><br> - <span class="text-secondary">RSSI:</span> {{ modem_data.rssi }}<br/> - {% if modem_data.sinr %} - <span class="text-secondary">SINR:</span> {{ modem_data.sinr }}<br/> - {% endif %} - <span class="text-secondary">Время соединения:</span> {{ modem_data.connected_time }}<br> - <span class="text-secondary">Принято/передано:</span> {{ modem_data.downloaded }} / {{ modem_data.uploaded }} - <br> - <a href="/modem/verbose/?modem={{ modem }}">Подробная информация</a> -{% else %} - {% include 'spinner.twig' %} -{% endif %}
\ No newline at end of file diff --git a/localwebsite/templates-web/modem_status_page.twig b/localwebsite/templates-web/modem_status_page.twig deleted file mode 100644 index 3f20b86..0000000 --- a/localwebsite/templates-web/modem_status_page.twig +++ /dev/null @@ -1,19 +0,0 @@ -{% include 'bc.twig' with { - history: [ - {text: "Модемы" } - ] -} %} - -{% for modem_key, modem in modems %} - <h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ modem.label }}</h6> - <div id="modem_data_{{ modem_key }}"> - {% include 'modem_data.twig' with { - loading: true, - modem: modem_key - } %} - </div> -{% endfor %} - -{% js %} -ModemStatus.init({{ js_modems|json_encode|raw }}); -{% endjs %} diff --git a/localwebsite/templates-web/modem_verbose_page.twig b/localwebsite/templates-web/modem_verbose_page.twig deleted file mode 100644 index 3b4c25e..0000000 --- a/localwebsite/templates-web/modem_verbose_page.twig +++ /dev/null @@ -1,15 +0,0 @@ -{% include 'bc.twig' with { - history: [ - {link: '/modem/', text: "Модемы" }, - {text: modem_name} - ] -} %} - -{% for item in data %} - {% set item_name = item[0] %} - {% set item_data = item[1] %} - <h6 class="text-primary mt-4">{{ item_name }}</h6> - {% for k, v in item_data %} - {{ k }} = {{ v }}<br> - {% endfor %} -{% endfor %}
\ No newline at end of file diff --git a/localwebsite/templates-web/spinner.twig b/localwebsite/templates-web/spinner.twig deleted file mode 100644 index 2d629ea..0000000 --- a/localwebsite/templates-web/spinner.twig +++ /dev/null @@ -1,14 +0,0 @@ -<div class="sk-fading-circle"> - <div class="sk-circle1 sk-circle"></div> - <div class="sk-circle2 sk-circle"></div> - <div class="sk-circle3 sk-circle"></div> - <div class="sk-circle4 sk-circle"></div> - <div class="sk-circle5 sk-circle"></div> - <div class="sk-circle6 sk-circle"></div> - <div class="sk-circle7 sk-circle"></div> - <div class="sk-circle8 sk-circle"></div> - <div class="sk-circle9 sk-circle"></div> - <div class="sk-circle10 sk-circle"></div> - <div class="sk-circle11 sk-circle"></div> - <div class="sk-circle12 sk-circle"></div> -</div>
\ No newline at end of file diff --git a/test/test_modems.py b/test/test_modems.py new file mode 100755 index 0000000..39981f7 --- /dev/null +++ b/test/test_modems.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import __py_include +from homekit.modem import E3372, ModemsConfig + + +if __name__ == '__main__': + mc = ModemsConfig() + modem = mc.get('mts-azov') + cl = E3372(modem['ip'], legacy_token_auth=modem['legacy_auth']) diff --git a/web/kbn_assets/app.css b/web/kbn_assets/app.css index 3146bcf..1a4697a 100644 --- a/web/kbn_assets/app.css +++ b/web/kbn_assets/app.css @@ -14,7 +14,7 @@ } -/** spinner.twig **/ +/** spinner.j2 **/ .sk-fading-circle { margin-top: 10px; diff --git a/web/kbn_assets/app.js b/web/kbn_assets/app.js index c187f89..eaac003 100644 --- a/web/kbn_assets/app.js +++ b/web/kbn_assets/app.js @@ -319,6 +319,26 @@ window.Cameras = { })(); +class ModemStatusUpdater { + constructor(id) { + this.id = id; + this.elem = ge('modem_data_'+id); + this.fetch() + } + + fetch() { + ajax.get('/modems/info.ajx', { + id: this.id + }).then(({response}) => { + const {html} = response; + this.elem.innerHTML = html; + + // TODO enqueue rerender + }); + } +} + + var ModemStatus = { _modems: [], @@ -329,21 +349,3 @@ var ModemStatus = { } } }; - -function ModemStatusUpdater(id) { - this.id = id; - this.elem = ge('modem_data_'+id); - this.fetch(); -} -extend(ModemStatusUpdater.prototype, { - fetch: function() { - ajax.get('/modem/get.ajax', { - id: this.id - }).then(({response}) => { - var {html} = response; - this.elem.innerHTML = html; - - // TODO enqueue rerender - }); - }, -});
\ No newline at end of file diff --git a/web/kbn_templates/base.j2 b/web/kbn_templates/base.j2 index d43a08b..e2e29e3 100644 --- a/web/kbn_templates/base.j2 +++ b/web/kbn_templates/base.j2 @@ -35,9 +35,9 @@ {% block content %}{% endblock %} -{% if js %} -<script>{{ js|raw }}</script> -{% endif %} +<script> +{% block js %}{% endblock %} +</script> </div> </body> diff --git a/web/kbn_templates/modem_data.j2 b/web/kbn_templates/modem_data.j2 new file mode 100644 index 0000000..7f97b77 --- /dev/null +++ b/web/kbn_templates/modem_data.j2 @@ -0,0 +1,13 @@ +{% with level=modem_data.level %} + <span class="text-secondary">Сигнал:</span> {% include 'signal_level.j2' %}<br> +{% endwith %} + +<span class="text-secondary">Тип сети:</span> <b>{{ modem_data.type }}</b><br> +<span class="text-secondary">RSSI:</span> {{ modem_data.rssi }}<br/> +{% if modem_data.sinr %} +<span class="text-secondary">SINR:</span> {{ modem_data.sinr }}<br/> +{% endif %} +<span class="text-secondary">Время соединения:</span> {{ modem_data.connected_time }}<br> +<span class="text-secondary">Принято/передано:</span> {{ modem_data.downloaded }} / {{ modem_data.uploaded }} +<br> +<a href="/modems/verbose.cgi?id={{ modem }}">Подробная информация</a> diff --git a/web/kbn_templates/modem_verbose.j2 b/web/kbn_templates/modem_verbose.j2 new file mode 100644 index 0000000..7c6c930 --- /dev/null +++ b/web/kbn_templates/modem_verbose.j2 @@ -0,0 +1,18 @@ +{% extends "base.j2" %} + +{% block content %} +{{ breadcrumbs([ + {'link': '/modems.cgi', 'text': "Модемы"}, + {'text': modem_name} +]) }} + +{% for item in data %} + {% set item_name = item[0] %} + {% set item_data = item[1] %} + <h6 class="text-primary mt-4">{{ item_name }}</h6> + {% for k, v in item_data.items() %} + {{ k }} = {{ v }}<br> + {% endfor %} +{% endfor %} + +{% endblock %}
\ No newline at end of file diff --git a/web/kbn_templates/modems.j2 b/web/kbn_templates/modems.j2 index f148140..4ff9cf8 100644 --- a/web/kbn_templates/modems.j2 +++ b/web/kbn_templates/modems.j2 @@ -9,4 +9,8 @@ {% include "loading.j2" %} </div> {% endfor %} +{% endblock %} + +{% block js %} +ModemStatus.init({{ modems.getkeys()|tojson }}); {% endblock %}
\ No newline at end of file diff --git a/localwebsite/templates-web/signal_level.twig b/web/kbn_templates/signal_level.j2 index 9498482..93c9abf 100644 --- a/localwebsite/templates-web/signal_level.twig +++ b/web/kbn_templates/signal_level.j2 @@ -1,5 +1,5 @@ <div class="signal_level"> - {% for i in 0..4 %} + {% for i in range(5) %} <div{% if i < level %} class="yes"{% endif %}></div> {% endfor %} </div>
\ No newline at end of file |