This commit is contained in:
Evgeny Sorokin 2024-01-16 02:05:00 +03:00
parent 7058d0f506
commit da5db8bc28
21 changed files with 433 additions and 496 deletions

View File

@ -2,16 +2,18 @@
import asyncio import asyncio
import jinja2 import jinja2
import aiohttp_jinja2 import aiohttp_jinja2
import json
import os import os
import re
import __py_include import __py_include
from io import StringIO from io import StringIO
from typing import Optional from typing import Optional, Union
from homekit.config import config, AppConfigUnit 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 aiohttp import web
from homekit import http from homekit import http
from homekit.modem import ModemsConfig from homekit.modem import ModemsConfig, E3372, MacroNetWorkType
class WebKbnConfig(AppConfigUnit): class WebKbnConfig(AppConfigUnit):
@ -49,7 +51,7 @@ def get_css_link(file, version) -> str:
def get_head_static() -> str: def get_head_static() -> str:
buf = StringIO() buf = StringIO()
for file in STATIC_FILES: for file in STATIC_FILES:
v = 1 v = 2
try: try:
q_ind = file.index('?') q_ind = file.index('?')
v = file[q_ind+1:] v = file[q_ind+1:]
@ -64,19 +66,52 @@ def get_head_static() -> str:
return buf.getvalue() 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): class WebSite(http.HTTPServer):
_modems_config: ModemsConfig
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._modems_config = ModemsConfig()
aiohttp_jinja2.setup( aiohttp_jinja2.setup(
self.app, 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.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets'))
self.get('/main.cgi', self.get_index) self.get('/main.cgi', self.get_index)
self.get('/modems.cgi', self.get_modems) 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, async def render_page(self,
req: http.Request, req: http.Request,
@ -99,19 +134,50 @@ class WebSite(http.HTTPServer):
title="Home web site") title="Home web site")
async def get_modems(self, req: http.Request): async def get_modems(self, req: http.Request):
mc = ModemsConfig()
print(mc)
return await self.render_page(req, 'modems', return await self.render_page(req, 'modems',
title='Состояние модемов', 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__': if __name__ == '__main__':
config.load_app(WebKbnConfig) 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 = WebSite(config.app_config['listen_addr'])
server.run() server.run()

View File

@ -78,6 +78,9 @@ class BaseConfigUnit(ABC):
raise KeyError(f'option {key} not found') raise KeyError(f'option {key} not found')
def getkeys(self):
return list(self._data.keys())
class ConfigUnit(BaseConfigUnit): class ConfigUnit(BaseConfigUnit):
NAME = 'dumb' NAME = 'dumb'

View File

@ -1,2 +1,2 @@
from .http import serve, ok, routes, HTTPServer from .http import serve, ok, routes, HTTPServer, HTTPMethod
from aiohttp.web import FileResponse, StreamResponse, Request, Response from aiohttp.web import FileResponse, StreamResponse, Request, Response

View File

@ -1,6 +1,7 @@
import logging import logging
import asyncio import asyncio
from enum import Enum
from aiohttp import web from aiohttp import web
from aiohttp.web import Response from aiohttp.web import Response
from aiohttp.web_exceptions import HTTPNotFound from aiohttp.web_exceptions import HTTPNotFound
@ -104,3 +105,8 @@ class HTTPServer:
def plain(self, text: str): def plain(self, text: str):
return Response(text=text, content_type='text/plain') return Response(text=text, content_type='text/plain')
class HTTPMethod(Enum):
GET = 'GET'
POST = 'POST'

View File

@ -1 +1,2 @@
from .config import ModemsConfig from .config import ModemsConfig
from .e3372 import E3372, MacroNetWorkType

View File

@ -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

View File

@ -12,7 +12,7 @@ import re
import os import os
from enum import Enum from enum import Enum
from datetime import datetime from datetime import datetime, timedelta
from typing import Optional, List from typing import Optional, List
from zlib import adler32 from zlib import adler32
@ -255,6 +255,25 @@ def filesize_fmt(num, suffix="B") -> str:
return f"{num:.1f} Yi{suffix}" 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): class HashableEnum(Enum):
def hash(self) -> int: def hash(self) -> int:
return adler32(self.name.encode()) return adler32(self.name.encode())

View File

@ -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 '?';
}
}
}

View File

@ -7,65 +7,6 @@ use libphonenumber\PhoneNumberUtil;
class ModemHandler extends RequestHandler 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() { public function GET_routing_smallhome_page() {
global $config; global $config;
@ -233,32 +174,6 @@ class ModemHandler extends RequestHandler
$go_back(); $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() { protected static function getCurrentUpstream() {
global $config; global $config;

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

9
test/test_modems.py Executable file
View File

@ -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'])

View File

@ -14,7 +14,7 @@
} }
/** spinner.twig **/ /** spinner.j2 **/
.sk-fading-circle { .sk-fading-circle {
margin-top: 10px; margin-top: 10px;

View File

@ -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 = { var ModemStatus = {
_modems: [], _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
});
},
});

View File

@ -35,9 +35,9 @@
{% block content %}{% endblock %} {% block content %}{% endblock %}
{% if js %} <script>
<script>{{ js|raw }}</script> {% block js %}{% endblock %}
{% endif %} </script>
</div> </div>
</body> </body>

View File

@ -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>

View File

@ -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 %}

View File

@ -10,3 +10,7 @@
</div> </div>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
{% block js %}
ModemStatus.init({{ modems.getkeys()|tojson }});
{% endblock %}

View File

@ -1,5 +1,5 @@
<div class="signal_level"> <div class="signal_level">
{% for i in 0..4 %} {% for i in range(5) %}
<div{% if i < level %} class="yes"{% endif %}></div> <div{% if i < level %} class="yes"{% endif %}></div>
{% endfor %} {% endfor %}
</div> </div>