aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2024-02-17 02:48:57 +0300
committerEvgeny Zinoviev <me@ch1p.io>2024-02-17 02:48:57 +0300
commitb7f1d55c9b4de4d21b11e5615a5dc8be0d4e883c (patch)
treedf3cba57518e21590d579b014867611002d92de5
parentc4ace358182d1f58724336714490e3caac6b60df (diff)
parent05c85757b8e2340441057d9ddfde2e9649ae8676 (diff)
Merge branch 'website-python-rewrite'
-rwxr-xr-xbin/ipcam_capture.py3
-rwxr-xr-xbin/ipcam_ntp_util.py199
-rwxr-xr-xbin/mqtt_node_util.py5
-rw-r--r--bin/web_kbn.py354
-rw-r--r--include/py/homekit/camera/config.py51
-rw-r--r--include/py/homekit/camera/types.py32
-rw-r--r--include/py/homekit/config/_configs.py1
-rw-r--r--include/py/homekit/config/config.py30
-rw-r--r--include/py/homekit/http/__init__.py4
-rw-r--r--include/py/homekit/http/http.py11
-rw-r--r--include/py/homekit/inverter/config.py4
-rw-r--r--include/py/homekit/modem/__init__.py2
-rw-r--r--include/py/homekit/modem/config.py29
-rw-r--r--include/py/homekit/modem/e3372.py253
-rw-r--r--include/py/homekit/mqtt/_config.py13
-rw-r--r--include/py/homekit/mqtt/module/temphum.py39
-rw-r--r--include/py/homekit/util.py67
-rw-r--r--localwebsite/classes/E3372.php310
-rw-r--r--localwebsite/classes/GPIORelaydClient.php18
-rw-r--r--localwebsite/classes/InverterdClient.php69
-rw-r--r--localwebsite/handlers/InverterHandler.php104
-rw-r--r--localwebsite/handlers/MiscHandler.php42
-rw-r--r--localwebsite/handlers/ModemHandler.php170
-rw-r--r--localwebsite/htdocs/assets/inverter.js15
-rw-r--r--localwebsite/htdocs/assets/modem.js29
-rw-r--r--localwebsite/htdocs/index.php9
-rw-r--r--localwebsite/templates-web/inverter_page.twig20
-rw-r--r--localwebsite/templates-web/modem_data.twig14
-rw-r--r--localwebsite/templates-web/modem_status_page.twig19
-rw-r--r--localwebsite/templates-web/modem_verbose_page.twig15
-rw-r--r--localwebsite/templates-web/spinner.twig14
-rw-r--r--requirements.txt8
-rw-r--r--tasks/df_h.sh2
-rwxr-xr-xtest/test_modems.py9
-rw-r--r--web/kbn_assets/app.css (renamed from localwebsite/htdocs/assets/app.css)2
-rw-r--r--web/kbn_assets/app.js (renamed from localwebsite/htdocs/assets/app.js)51
-rw-r--r--web/kbn_assets/bootstrap.min.css (renamed from localwebsite/htdocs/assets/bootstrap.min.css)0
-rw-r--r--web/kbn_assets/bootstrap.min.js (renamed from localwebsite/htdocs/assets/bootstrap.min.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/h265webjs-v20221106-reminified.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106-reminified.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/h265webjs-v20221106.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-120func-v20221120.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm)bin2190151 -> 2190151 bytes
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-120func.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-120func.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm)bin2108889 -> 2108889 bytes
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-256mb.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm)bin2108889 -> 2108889 bytes
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-512mb.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-format.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-format.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-v20221120.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/missile-v20221120.wasm (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm)bin2108891 -> 2108891 bytes
-rw-r--r--web/kbn_assets/h265webjs-dist/missile.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/raw-parser.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/raw-parser.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/worker-fetch-dist.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/worker-fetch-dist.js)0
-rw-r--r--web/kbn_assets/h265webjs-dist/worker-parse-dist.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/worker-parse-dist.js)0
-rw-r--r--web/kbn_assets/hls.js (renamed from localwebsite/htdocs/assets/hls.js)0
-rw-r--r--web/kbn_assets/polyfills.js (renamed from localwebsite/htdocs/assets/polyfills.js)0
-rw-r--r--web/kbn_templates/base.j244
-rw-r--r--web/kbn_templates/index.j239
-rw-r--r--web/kbn_templates/inverter.j220
-rw-r--r--web/kbn_templates/loading.j214
-rw-r--r--web/kbn_templates/modem_data.j213
-rw-r--r--web/kbn_templates/modem_verbose.j218
-rw-r--r--web/kbn_templates/modems.j216
-rw-r--r--web/kbn_templates/pump.j2 (renamed from localwebsite/templates-web/pump.twig)16
-rw-r--r--web/kbn_templates/signal_level.j2 (renamed from localwebsite/templates-web/signal_level.twig)2
-rw-r--r--web/kbn_templates/sms.j2 (renamed from localwebsite/templates-web/sms_page.twig)31
68 files changed, 1275 insertions, 955 deletions
diff --git a/bin/ipcam_capture.py b/bin/ipcam_capture.py
index 5de14af..226e12e 100755
--- a/bin/ipcam_capture.py
+++ b/bin/ipcam_capture.py
@@ -48,7 +48,8 @@ async def run_ffmpeg(cam: int, channel: int):
else:
debug_args = ['-nostats', '-loglevel', 'error']
- protocol = 'tcp' if ipcam_config.should_use_tcp_for_rtsp(cam) else 'udp'
+ # protocol = 'tcp' if ipcam_config.should_use_tcp_for_rtsp(cam) else 'udp'
+ protocol = 'tcp'
user, pw = ipcam_config.get_rtsp_creds()
ip = ipcam_config.get_camera_ip(cam)
path = ipcam_config.get_camera_type(cam).get_channel_url(channel)
diff --git a/bin/ipcam_ntp_util.py b/bin/ipcam_ntp_util.py
new file mode 100755
index 0000000..98639bd
--- /dev/null
+++ b/bin/ipcam_ntp_util.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+import __py_include
+import requests
+import hashlib
+import xml.etree.ElementTree as ET
+
+from time import time
+from argparse import ArgumentParser, ArgumentError
+from homekit.util import validate_ipv4, validate_ipv4_or_hostname
+from homekit.camera import IpcamConfig
+
+
+def xml_to_dict(xml_data: str) -> dict:
+ # Parse the XML data
+ root = ET.fromstring(xml_data)
+
+ # Function to remove namespace from the tag name
+ def remove_namespace(tag):
+ return tag.split('}')[-1] # Splits on '}' and returns the last part, the actual tag name without namespace
+
+ # Function to recursively convert XML elements to a dictionary
+ def elem_to_dict(elem):
+ tag = remove_namespace(elem.tag)
+ elem_dict = {tag: {}}
+
+ # If the element has attributes, add them to the dictionary
+ elem_dict[tag].update({'@' + remove_namespace(k): v for k, v in elem.attrib.items()})
+
+ # Handle the element's text content, if present and not just whitespace
+ text = elem.text.strip() if elem.text and elem.text.strip() else None
+ if text:
+ elem_dict[tag]['#text'] = text
+
+ # Process child elements
+ for child in elem:
+ child_dict = elem_to_dict(child)
+ child_tag = remove_namespace(child.tag)
+ if child_tag not in elem_dict[tag]:
+ elem_dict[tag][child_tag] = []
+ elem_dict[tag][child_tag].append(child_dict[child_tag])
+
+ # Simplify structure if there's only text or no children and no attributes
+ if len(elem_dict[tag]) == 1 and '#text' in elem_dict[tag]:
+ return {tag: elem_dict[tag]['#text']}
+ elif not elem_dict[tag]:
+ return {tag: ''}
+
+ return elem_dict
+
+ # Convert the root element to dictionary
+ return elem_to_dict(root)
+
+
+def sha256_hex(input_string: str) -> str:
+ return hashlib.sha256(input_string.encode()).hexdigest()
+
+
+class ResponseError(RuntimeError):
+ pass
+
+
+class AuthError(ResponseError):
+ pass
+
+
+class HikvisionISAPIClient:
+ def __init__(self, host):
+ self.host = host
+ self.cookies = {}
+
+ def auth(self, username: str, password: str):
+ r = requests.get(self.isapi_uri('Security/sessionLogin/capabilities'),
+ {'username': username},
+ headers={
+ 'X-Requested-With': 'XMLHttpRequest',
+ })
+ r.raise_for_status()
+ caps = xml_to_dict(r.text)['SessionLoginCap']
+ is_irreversible = caps['isIrreversible'][0].lower() == 'true'
+
+ # https://github.com/JakeVincet/nvt/blob/master/2018/hikvision/gb_hikvision_ip_camera_default_credentials.nasl
+ # also look into webAuth.js and utils.js
+
+ if 'salt' in caps and is_irreversible:
+ p = sha256_hex(username + caps['salt'][0] + password)
+ p = sha256_hex(p + caps['challenge'][0])
+ for i in range(int(caps['iterations'][0])-2):
+ p = sha256_hex(p)
+ else:
+ p = sha256_hex(password) + caps['challenge'][0]
+ for i in range(int(caps['iterations'][0])-1):
+ p = sha256_hex(p)
+
+ data = '<SessionLogin>'
+ data += f'<userName>{username}</userName>'
+ data += f'<password>{p}</password>'
+ data += f'<sessionID>{caps["sessionID"][0]}</sessionID>'
+ data += '<isSessionIDValidLongTerm>false</isSessionIDValidLongTerm>'
+ data += f'<sessionIDVersion>{caps["sessionIDVersion"][0]}</sessionIDVersion>'
+ data += '</SessionLogin>'
+
+ r = requests.post(self.isapi_uri(f'Security/sessionLogin?timeStamp={int(time())}'), data=data, headers={
+ 'Accept-Encoding': 'gzip, deflate',
+ 'If-Modified-Since': '0',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+ })
+ r.raise_for_status()
+ resp = xml_to_dict(r.text)['SessionLogin']
+ status_value = int(resp['statusValue'][0])
+ status_string = resp['statusString'][0]
+ if status_value != 200:
+ raise AuthError(f'{status_value}: {status_string}')
+
+ self.cookies = r.cookies.get_dict()
+
+ def get_ntp_server(self) -> str:
+ r = requests.get(self.isapi_uri('System/time/ntpServers/capabilities'), cookies=self.cookies)
+ r.raise_for_status()
+ ntp_server = xml_to_dict(r.text)['NTPServerList']['NTPServer'][0]
+
+ if ntp_server['addressingFormatType'][0]['#text'] == 'hostname':
+ ntp_host = ntp_server['hostName'][0]
+ else:
+ ntp_host = ntp_server['ipAddress'][0]
+
+ return ntp_host
+
+ def set_timezone(self):
+ data = '<?xml version="1.0" encoding="UTF-8"?>'
+ data += '<Time><timeMode>NTP</timeMode><timeZone>CST-3:00:00</timeZone></Time>'
+
+ r = requests.put(self.isapi_uri('System/time'), cookies=self.cookies, data=data, headers={
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
+ })
+ self.isapi_check_put_response(r)
+
+ def set_ntp_server(self, ntp_host: str, ntp_port: int = 123):
+ format = 'ipaddress' if validate_ipv4(ntp_host) else 'hostname'
+
+ data = '<?xml version="1.0" encoding="UTF-8"?>'
+ data += f'<NTPServer><id>1</id><addressingFormatType>{format}</addressingFormatType><ipAddress>{ntp_host}</ipAddress><portNo>{ntp_port}</portNo><synchronizeInterval>1440</synchronizeInterval></NTPServer>'
+
+ r = requests.put(self.isapi_uri('System/time/ntpServers/1'),
+ data=data,
+ cookies=self.cookies,
+ headers={
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
+ })
+ self.isapi_check_put_response(r)
+
+ def isapi_uri(self, path: str) -> str:
+ return f'http://{self.host}/ISAPI/{path}'
+
+ def isapi_check_put_response(self, r):
+ r.raise_for_status()
+ resp = xml_to_dict(r.text)['ResponseStatus']
+
+ status_code = int(resp['statusCode'][0])
+ status_string = resp['statusString'][0]
+
+ if status_code != 1 or status_string.lower() != 'ok':
+ raise ResponseError('response status looks bad')
+
+
+def main():
+ parser = ArgumentParser()
+ parser.add_argument('--host', type=str, required=True)
+ parser.add_argument('--get-ntp-server', action='store_true')
+ parser.add_argument('--set-ntp-server', type=str)
+ parser.add_argument('--username', type=str)
+ parser.add_argument('--password', type=str)
+ args = parser.parse_args()
+
+ if not args.get_ntp_server and not args.set_ntp_server:
+ raise ArgumentError(None, 'either --get-ntp-server or --set-ntp-server is required')
+
+ ipcam_config = IpcamConfig()
+ login = args.username if args.username else ipcam_config['web_creds']['login']
+ password = args.password if args.password else ipcam_config['web_creds']['password']
+
+ client = HikvisionISAPIClient(args.host)
+ client.auth(args.username, args.password)
+
+ if args.get_ntp_server:
+ print(client.get_ntp_server())
+ return
+
+ if not args.set_ntp_server:
+ raise ArgumentError(None, '--set-ntp-server is required')
+
+ if not validate_ipv4_or_hostname(args.set_ntp_server):
+ raise ArgumentError(None, 'input ntp server is neither ip address nor a valid hostname')
+
+ client.set_ntp_server(args.set_ntp_server)
+
+
+if __name__ == '__main__':
+ main() \ No newline at end of file
diff --git a/bin/mqtt_node_util.py b/bin/mqtt_node_util.py
index 68d3bd1..639d4b9 100755
--- a/bin/mqtt_node_util.py
+++ b/bin/mqtt_node_util.py
@@ -48,7 +48,6 @@ if __name__ == '__main__':
help='mqtt modules to include')
parser.add_argument('--switch-relay', choices=[0, 1], type=int,
help='send relay state')
- parser.add_argument('--legacy-relay', action='store_true')
parser.add_argument('--push-ota', type=str, metavar='OTA_FILENAME',
help='push OTA, receives path to firmware.bin (not .elf!)')
parser.add_argument('--no-wait', action='store_true',
@@ -80,8 +79,10 @@ if __name__ == '__main__':
if arg.modules:
for m in arg.modules:
kwargs = {}
- if m == 'relay' and arg.legacy_relay:
+ if m == 'relay' and MqttNodesConfig().node_uses_legacy_relay_power_payload(arg.node_id):
kwargs['legacy_topics'] = True
+ if m == 'temphum' and MqttNodesConfig().node_uses_legacy_temphum_data_payload(arg.node_id):
+ kwargs['legacy_payload'] = True
module_instance = mqtt_node.load_module(m, **kwargs)
if m == 'relay' and arg.switch_relay is not None:
relay_module = module_instance
diff --git a/bin/web_kbn.py b/bin/web_kbn.py
new file mode 100644
index 0000000..c21269b
--- /dev/null
+++ b/bin/web_kbn.py
@@ -0,0 +1,354 @@
+#!/usr/bin/env python3
+import asyncio
+import jinja2
+import aiohttp_jinja2
+import json
+import re
+import inverterd
+import phonenumbers
+import __py_include
+
+from io import StringIO
+from aiohttp.web import HTTPFound
+from typing import Optional, Union
+from homekit.config import config, AppConfigUnit
+from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string
+from homekit.modem import E3372, ModemsConfig, MacroNetWorkType
+from homekit.inverter.config import InverterdConfig
+from homekit.relay.sunxi_h3_client import RelayClient
+from homekit import http
+
+
+class WebKbnConfig(AppConfigUnit):
+ NAME = 'web_kbn'
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ 'listen_addr': cls._addr_schema(required=True),
+ 'assets_public_path': {'type': 'string'},
+ 'pump_addr': cls._addr_schema(required=True),
+ 'inverter_grafana_url': {'type': 'string'},
+ 'sensors_grafana_url': {'type': 'string'},
+ }
+
+
+STATIC_FILES = [
+ 'bootstrap.min.css',
+ 'bootstrap.min.js',
+ 'polyfills.js',
+ 'app.js',
+ 'app.css'
+]
+
+
+def get_js_link(file, version) -> str:
+ if version:
+ file += f'?version={version}'
+ return f'<script src="{config.app_config["assets_public_path"]}/{file}" type="text/javascript"></script>'
+
+
+def get_css_link(file, version) -> str:
+ if version:
+ file += f'?version={version}'
+ return f'<link rel="stylesheet" type="text/css" href="{config.app_config["assets_public_path"]}/{file}">'
+
+
+def get_head_static() -> str:
+ buf = StringIO()
+ for file in STATIC_FILES:
+ v = 2
+ try:
+ q_ind = file.index('?')
+ v = file[q_ind+1:]
+ file = file[:file.index('?')]
+ except ValueError:
+ pass
+
+ if file.endswith('.js'):
+ buf.write(get_js_link(file, v))
+ else:
+ buf.write(get_css_link(file, v))
+ return buf.getvalue()
+
+
+def get_modem_client(modem_cfg: dict) -> E3372:
+ return E3372(modem_cfg['ip'], legacy_token_auth=modem_cfg['legacy_auth'])
+
+
+def get_modem_data(modem_cfg: dict, get_raw=False) -> Union[dict, tuple]:
+ cl = get_modem_client(modem_cfg)
+
+ 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']))
+ }
+
+
+def get_pump_client() -> RelayClient:
+ addr = config.app_config['pump_addr']
+ cl = RelayClient(host=addr.host, port=addr.port)
+ cl.connect()
+ return cl
+
+
+def get_inverter_client() -> inverterd.Client:
+ cl = inverterd.Client(host=InverterdConfig()['remote_addr'].host)
+ cl.connect()
+ cl.format(inverterd.Format.JSON)
+ return cl
+
+
+def get_inverter_data() -> tuple:
+ cl = get_inverter_client()
+
+ status = json.loads(cl.exec('get-status'))['data']
+ rated = json.loads(cl.exec('get-rated'))['data']
+
+ power_direction = status['battery_power_direction'].lower()
+ power_direction = re.sub('ge$', 'ging', power_direction)
+
+ charging_rate = ''
+ if power_direction == 'charging':
+ charging_rate = ' @ %s %s' % (
+ status['battery_charge_current']['value'],
+ status['battery_charge_current']['unit'])
+ elif power_direction == 'discharging':
+ charging_rate = ' @ %s %s' % (
+ status['battery_discharge_current']['value'],
+ status['battery_discharge_current']['unit'])
+
+ html = '<b>Battery:</b> %s %s' % (
+ status['battery_voltage']['value'],
+ status['battery_voltage']['unit'])
+ html += ' (%s%s, ' % (
+ status['battery_capacity']['value'],
+ status['battery_capacity']['unit'])
+ html += '%s%s)' % (power_direction, charging_rate)
+
+ html += "\n"
+ html += '<b>Load:</b> %s %s' % (
+ status['ac_output_active_power']['value'],
+ status['ac_output_active_power']['unit'])
+ html += ' (%s%%)' % (status['output_load_percent']['value'],)
+
+ if status['pv1_input_power']['value'] > 0:
+ html += "\n"
+ html += '<b>Input power:</b> %s %s' % (
+ status['pv1_input_power']['value'],
+ status['pv1_input_power']['unit'])
+
+ if status['grid_voltage']['value'] > 0 or status['grid_freq']['value'] > 0:
+ html += "\n"
+ html += '<b>AC input:</b> %s %s' % (
+ status['grid_voltage']['value'],
+ status['grid_voltage']['unit'])
+ html += ', %s %s' % (
+ status['grid_freq']['value'],
+ status['grid_freq']['unit'])
+
+ html += "\n"
+ html += '<b>Priority:</b> %s' % (rated['output_source_priority'],)
+
+ html = html.replace("\n", '<br>')
+
+ return status, rated, html
+
+
+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')),
+ 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.index)
+
+ self.get('/modems.cgi', self.modems)
+ self.get('/modems/info.ajx', self.modems_ajx)
+ self.get('/modems/verbose.cgi', self.modems_verbose)
+
+ self.get('/inverter.cgi', self.inverter)
+ self.get('/inverter.ajx', self.inverter_ajx)
+ self.get('/pump.cgi', self.pump)
+ self.get('/sms.cgi', self.sms)
+ self.post('/sms.cgi', self.sms_post)
+
+ async def render_page(self,
+ req: http.Request,
+ template_name: str,
+ title: Optional[str] = None,
+ context: Optional[dict] = None):
+ if context is None:
+ context = {}
+ context = {
+ **context,
+ 'head_static': get_head_static()
+ }
+ if title is not None:
+ context['title'] = title
+ response = aiohttp_jinja2.render_template(template_name+'.j2', req, context=context)
+ return response
+
+ async def index(self, req: http.Request):
+ ctx = {}
+ for k in 'inverter', 'sensors':
+ ctx[f'{k}_grafana_url'] = config.app_config[f'{k}_grafana_url']
+ return await self.render_page(req, 'index',
+ title="Home web site",
+ context=ctx)
+
+ async def modems(self, req: http.Request):
+ return await self.render_page(req, 'modems',
+ title='Состояние модемов',
+ context=dict(modems=self._modems_config))
+
+ async def modems_ajx(self, req: http.Request):
+ modem = req.query.get('id', None)
+ if modem not in self._modems_config.keys():
+ 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 modems_verbose(self, req: http.Request):
+ modem = req.query.get('id', None)
+ if modem not in self._modems_config.keys():
+ 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))
+
+ async def sms(self, req: http.Request):
+ modem = req.query.get('id', list(self._modems_config.keys())[0])
+ is_outbox = int(req.query.get('outbox', 0)) == 1
+ error = req.query.get('error', None)
+ sent = int(req.query.get('sent', 0)) == 1
+
+ cl = get_modem_client(self._modems_config[modem])
+ messages = cl.sms_list(1, 20, is_outbox)
+ return await self.render_page(req, 'sms',
+ title=f"SMS-сообщения ({'исходящие' if is_outbox else 'входящие'}, {modem})",
+ context=dict(
+ modems=self._modems_config,
+ selected_modem=modem,
+ is_outbox=is_outbox,
+ error=error,
+ is_sent=sent,
+ messages=messages
+ ))
+
+ async def sms_post(self, req: http.Request):
+ modem = req.query.get('id', list(self._modems_config.keys())[0])
+ is_outbox = int(req.query.get('outbox', 0)) == 1
+
+ fd = await req.post()
+ phone = fd.get('phone', None)
+ text = fd.get('text', None)
+
+ return_url = f'/sms.cgi?id={modem}&outbox={int(is_outbox)}'
+ phone = re.sub('\s+', '', phone)
+
+ if len(phone) > 4:
+ country = None
+ if not phone.startswith('+'):
+ country = 'RU'
+ number = phonenumbers.parse(phone, country)
+ if not phonenumbers.is_valid_number(number):
+ raise HTTPFound(f'{return_url}&error=Неверный+номер')
+ phone = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
+
+ cl = get_modem_client(self._modems_config[modem])
+ cl.sms_send(phone, text)
+ raise HTTPFound(return_url)
+
+ async def inverter(self, req: http.Request):
+ action = req.query.get('do', None)
+ if action == 'set-osp':
+ val = req.query.get('value')
+ if val not in ('sub', 'sbu'):
+ raise ValueError('invalid osp value')
+ cl = get_inverter_client()
+ cl.exec('set-output-source-priority',
+ arguments=(val.upper(),))
+ raise HTTPFound('/inverter.cgi')
+
+ status, rated, html = await asyncio.get_event_loop().run_in_executor(None, get_inverter_data)
+ return await self.render_page(req, 'inverter',
+ title='Инвертор',
+ context=dict(status=status, rated=rated, html=html))
+
+ async def inverter_ajx(self, req: http.Request):
+ status, rated, html = await asyncio.get_event_loop().run_in_executor(None, get_inverter_data)
+ return self.ok({'html': html})
+
+ async def pump(self, req: http.Request):
+ # TODO
+ # these are blocking calls
+ # should be rewritten using aio
+
+ cl = get_pump_client()
+
+ action = req.query.get('set', None)
+ if action in ('on', 'off'):
+ getattr(cl, action)()
+ raise HTTPFound('/pump.cgi')
+
+ status = cl.status()
+ return await self.render_page(req, 'pump',
+ title='Насос',
+ context=dict(status=status))
+
+
+if __name__ == '__main__':
+ config.load_app(WebKbnConfig)
+
+ server = WebSite(config.app_config['listen_addr'])
+ server.run()
diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py
index c7dbc38..8aeb392 100644
--- a/include/py/homekit/camera/config.py
+++ b/include/py/homekit/camera/config.py
@@ -23,17 +23,13 @@ class IpcamConfig(ConfigUnit):
@classmethod
def schema(cls) -> Optional[dict]:
return {
- 'cams': {
+ 'cameras': {
'type': 'dict',
'keysrules': {'type': ['string', 'integer']},
'valuesrules': {
'type': 'dict',
'schema': {
'type': {'type': 'string', 'allowed': [t.value for t in CameraType], 'required': True},
- 'codec': {'type': 'string', 'allowed': [t.value for t in VideoCodecType], 'required': True},
- 'container': {'type': 'string', 'allowed': [t.value for t in VideoContainerType], 'required': True},
- 'server': {'type': 'string', 'allowed': list(_lbc.get().keys()), 'required': True},
- 'disk': {'type': 'integer', 'required': True},
'motion': {
'type': 'dict',
'schema': {
@@ -44,10 +40,18 @@ class IpcamConfig(ConfigUnit):
}
}
},
- 'rtsp_tcp': {'type': 'boolean'}
}
}
},
+ 'areas': {
+ 'type': 'dict',
+ 'keysrules': {'type': 'string'},
+ 'valuesrules': {
+ 'type': 'list',
+ 'schema': {'type': ['string', 'integer']} # same type as for 'cameras' keysrules
+ }
+ },
+ 'camera_ip_template': {'type': 'string', 'required': True},
'motion_padding': {'type': 'integer', 'required': True},
'motion_telegram': {'type': 'boolean', 'required': True},
'fix_interval': {'type': 'integer', 'required': True},
@@ -69,6 +73,15 @@ class IpcamConfig(ConfigUnit):
'login': {'type': 'string', 'required': True},
'password': {'type': 'string', 'required': True},
}
+ },
+
+ 'web_creds': {
+ 'required': True,
+ 'type': 'dict',
+ 'schema': {
+ 'login': {'type': 'string', 'required': True},
+ 'password': {'type': 'string', 'required': True},
+ }
}
}
@@ -94,6 +107,7 @@ class IpcamConfig(ConfigUnit):
}
}
+ # FIXME
def get_all_cam_names(self,
filter_by_server: Optional[str] = None,
filter_by_disk: Optional[int] = None) -> list[int]:
@@ -106,25 +120,22 @@ class IpcamConfig(ConfigUnit):
cams.append(int(cam))
return cams
- def get_all_cam_names_for_this_server(self,
- filter_by_disk: Optional[int] = None):
- return self.get_all_cam_names(filter_by_server=socket.gethostname(),
- filter_by_disk=filter_by_disk)
+ # def get_all_cam_names_for_this_server(self,
+ # filter_by_disk: Optional[int] = None):
+ # return self.get_all_cam_names(filter_by_server=socket.gethostname(),
+ # filter_by_disk=filter_by_disk)
- def get_cam_server_and_disk(self, cam: int) -> tuple[str, int]:
- return self['cams'][cam]['server'], self['cams'][cam]['disk']
+ # def get_cam_server_and_disk(self, cam: int) -> tuple[str, int]:
+ # return self['cams'][cam]['server'], self['cams'][cam]['disk']
- def get_camera_container(self, cam: int) -> VideoContainerType:
- return VideoContainerType(self['cams'][cam]['container'])
+ def get_camera_container(self, camera: int) -> VideoContainerType:
+ return self.get_camera_type(camera).get_container()
- def get_camera_type(self, cam: int) -> CameraType:
- return CameraType(self['cams'][cam]['type'])
+ def get_camera_type(self, camera: int) -> CameraType:
+ return CameraType(self['cams'][camera]['type'])
def get_rtsp_creds(self) -> tuple[str, str]:
return self['rtsp_creds']['login'], self['rtsp_creds']['password']
- def should_use_tcp_for_rtsp(self, cam: int) -> bool:
- return 'rtsp_tcp' in self['cams'][cam] and self['cams'][cam]['rtsp_tcp']
-
def get_camera_ip(self, camera: int) -> str:
- return f'192.168.5.{camera}'
+ return self['camera_ip_template'] % (str(camera),)
diff --git a/include/py/homekit/camera/types.py b/include/py/homekit/camera/types.py
index c313b58..da0fcc6 100644
--- a/include/py/homekit/camera/types.py
+++ b/include/py/homekit/camera/types.py
@@ -1,10 +1,21 @@
from enum import Enum
+class VideoContainerType(Enum):
+ MP4 = 'mp4'
+ MOV = 'mov'
+
+
+class VideoCodecType(Enum):
+ H264 = 'h264'
+ H265 = 'h265'
+
+
class CameraType(Enum):
ESP32 = 'esp32'
ALIEXPRESS_NONAME = 'ali'
- HIKVISION = 'hik'
+ HIKVISION_264 = 'hik_264'
+ HIKVISION_265 = 'hik_265'
def get_channel_url(self, channel: int) -> str:
if channel not in (1, 2):
@@ -12,22 +23,23 @@ class CameraType(Enum):
if channel == 1:
return ''
elif channel == 2:
- if self.value == CameraType.HIKVISION:
+ if self.value in (CameraType.HIKVISION_264, CameraType.HIKVISION_265):
return '/Streaming/Channels/2'
elif self.value == CameraType.ALIEXPRESS_NONAME:
return '/?stream=1.sdp'
else:
raise ValueError(f'unsupported camera type {self.value}')
+ def get_codec(self, channel: int) -> VideoCodecType:
+ if channel == 1:
+ return VideoCodecType.H264 if self.value == CameraType.HIKVISION_264 else VideoCodecType.H265
+ elif channel == 2:
+ return VideoCodecType.H265 if self.value == CameraType.ALIEXPRESS_NONAME else VideoCodecType.H264
+ else:
+ raise ValueError(f'unexpected channel {channel}')
-class VideoContainerType(Enum):
- MP4 = 'mp4'
- MOV = 'mov'
-
-
-class VideoCodecType(Enum):
- H264 = 'h264'
- H265 = 'h265'
+ def get_container(self) -> VideoContainerType:
+ return VideoContainerType.MP4 if self.get_codec(1) == VideoCodecType.H264 else VideoContainerType.MOV
class TimeFilterType(Enum):
diff --git a/include/py/homekit/config/_configs.py b/include/py/homekit/config/_configs.py
index f88c8ea..2cd2aca 100644
--- a/include/py/homekit/config/_configs.py
+++ b/include/py/homekit/config/_configs.py
@@ -26,6 +26,7 @@ class LinuxBoardsConfig(ConfigUnit):
'schema': {
'mdns': {'type': 'string', 'required': True},
'board': {'type': 'string', 'required': True},
+ 'location': {'type': 'string', 'required': True},
'network': {
'type': 'list',
'required': True,
diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py
index 5fe1ae8..fec92a6 100644
--- a/include/py/homekit/config/config.py
+++ b/include/py/homekit/config/config.py
@@ -41,6 +41,9 @@ class BaseConfigUnit(ABC):
self._data = {}
self._logger = logging.getLogger(self.__class__.__name__)
+ def __iter__(self):
+ return iter(self._data)
+
def __getitem__(self, key):
return self._data[key]
@@ -75,6 +78,15 @@ class BaseConfigUnit(ABC):
raise KeyError(f'option {key} not found')
+ def values(self):
+ return self._data.values()
+
+ def keys(self):
+ return self._data.keys()
+
+ def items(self):
+ return self._data.items()
+
class ConfigUnit(BaseConfigUnit):
NAME = 'dumb'
@@ -123,10 +135,10 @@ class ConfigUnit(BaseConfigUnit):
return None
@classmethod
- def _addr_schema(cls, required=False, **kwargs):
+ def _addr_schema(cls, required=False, only_ip=False, **kwargs):
return {
'type': 'addr',
- 'coerce': Addr.fromstring,
+ 'coerce': Addr.fromstring if not only_ip else Addr.fromipstring,
'required': required,
**kwargs
}
@@ -158,6 +170,7 @@ class ConfigUnit(BaseConfigUnit):
pass
v = MyValidator()
+ need_document = False
if rst == RootSchemaType.DICT:
normalized = v.validated({'document': self._data},
@@ -165,16 +178,21 @@ class ConfigUnit(BaseConfigUnit):
'type': 'dict',
'keysrules': {'type': 'string'},
'valuesrules': schema
- }})['document']
+ }})
+ need_document = True
elif rst == RootSchemaType.LIST:
v = MyValidator()
- normalized = v.validated({'document': self._data}, {'document': schema})['document']
+ normalized = v.validated({'document': self._data}, {'document': schema})
+ need_document = True
else:
normalized = v.validated(self._data, schema)
if not normalized:
raise cerberus.DocumentError(f'validation failed: {v.errors}')
+ if need_document:
+ normalized = normalized['document']
+
self._data = normalized
try:
@@ -235,6 +253,8 @@ class TranslationUnit(BaseConfigUnit):
class Translation:
LANGUAGES = ('en', 'ru')
+ DEFAULT_LANGUAGE = 'ru'
+
_langs: dict[str, TranslationUnit]
def __init__(self, name: str):
@@ -278,9 +298,7 @@ class Config:
and not isinstance(name, bool) \
and issubclass(name, AppConfigUnit) or name == AppConfigUnit:
self.app_name = name.NAME
- print(self.app_config)
self.app_config = name()
- print(self.app_config)
app_config = self.app_config
else:
self.app_name = name if isinstance(name, str) else None
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..82c5aae 100644
--- a/include/py/homekit/http/http.py
+++ b/include/py/homekit/http/http.py
@@ -1,8 +1,9 @@
import logging
import asyncio
+from enum import Enum
from aiohttp import web
-from aiohttp.web import Response
+from aiohttp.web import Response, HTTPFound
from aiohttp.web_exceptions import HTTPNotFound
from ..util import stringify, format_tb, Addr
@@ -20,6 +21,9 @@ async def errors_handler_middleware(request, handler):
except HTTPNotFound:
return web.json_response({'error': 'not found'}, status=404)
+ except HTTPFound as exc:
+ raise exc
+
except Exception as exc:
_logger.exception(exc)
data = {
@@ -104,3 +108,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/inverter/config.py b/include/py/homekit/inverter/config.py
index e284dfe..694ddae 100644
--- a/include/py/homekit/inverter/config.py
+++ b/include/py/homekit/inverter/config.py
@@ -8,6 +8,6 @@ class InverterdConfig(ConfigUnit):
@classmethod
def schema(cls) -> Optional[dict]:
return {
- 'remote_addr': {'type': 'string'},
- 'local_addr': {'type': 'string'},
+ 'remote_addr': cls._addr_schema(required=True),
+ 'local_addr': cls._addr_schema(required=True),
} \ No newline at end of file
diff --git a/include/py/homekit/modem/__init__.py b/include/py/homekit/modem/__init__.py
new file mode 100644
index 0000000..ea0930e
--- /dev/null
+++ b/include/py/homekit/modem/__init__.py
@@ -0,0 +1,2 @@
+from .config import ModemsConfig
+from .e3372 import E3372, MacroNetWorkType
diff --git a/include/py/homekit/modem/config.py b/include/py/homekit/modem/config.py
new file mode 100644
index 0000000..16d1ba0
--- /dev/null
+++ b/include/py/homekit/modem/config.py
@@ -0,0 +1,29 @@
+from ..config import ConfigUnit, Translation
+from typing import Optional
+
+
+class ModemsConfig(ConfigUnit):
+ NAME = 'modems'
+
+ _strings: Translation
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._strings = Translation('modems')
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ 'type': 'dict',
+ 'schema': {
+ 'ip': cls._addr_schema(required=True, only_ip=True),
+ 'gateway_ip': cls._addr_schema(required=False, only_ip=True),
+ 'legacy_auth': {'type': 'boolean', 'required': True}
+ }
+ }
+
+ def getshortname(self, modem: str, lang=Translation.DEFAULT_LANGUAGE):
+ return self._strings.get(lang)[modem]['short']
+
+ def getfullname(self, modem: str, lang=Translation.DEFAULT_LANGUAGE):
+ return self._strings.get(lang)[modem]['full'] \ No newline at end of file
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/mqtt/_config.py b/include/py/homekit/mqtt/_config.py
index 4916d8a..8aa3bfe 100644
--- a/include/py/homekit/mqtt/_config.py
+++ b/include/py/homekit/mqtt/_config.py
@@ -92,6 +92,7 @@ class MqttNodesConfig(ConfigUnit):
'type': 'dict',
'schema': {
'module': {'type': 'string', 'required': True, 'allowed': ['si7021', 'dht12']},
+ 'legacy_payload': {'type': 'boolean', 'required': False, 'default': False},
'interval': {'type': 'integer'},
'i2c_bus': {'type': 'integer'},
'tcpserver': {
@@ -168,3 +169,15 @@ class MqttNodesConfig(ConfigUnit):
else:
resdict[name] = node
return reslist if only_names else resdict
+
+ def node_uses_legacy_temphum_data_payload(self, node_id: str) -> bool:
+ try:
+ return self.get_node(node_id)['temphum']['legacy_payload']
+ except KeyError:
+ return False
+
+ def node_uses_legacy_relay_power_payload(self, node_id: str) -> bool:
+ try:
+ return self.get_node(node_id)['relay']['legacy_topics']
+ except KeyError:
+ return False
diff --git a/include/py/homekit/mqtt/module/temphum.py b/include/py/homekit/mqtt/module/temphum.py
index fd02cca..6deccfe 100644
--- a/include/py/homekit/mqtt/module/temphum.py
+++ b/include/py/homekit/mqtt/module/temphum.py
@@ -10,8 +10,8 @@ MODULE_NAME = 'MqttTempHumModule'
DATA_TOPIC = 'temphum/data'
-class MqttTemphumDataPayload(MqttPayload):
- FORMAT = '=ddb'
+class MqttTemphumLegacyDataPayload(MqttPayload):
+ FORMAT = '=dd'
UNPACKER = {
'temp': two_digits_precision,
'rh': two_digits_precision
@@ -19,39 +19,26 @@ class MqttTemphumDataPayload(MqttPayload):
temp: float
rh: float
- error: int
-# class MqttTempHumNodes(HashableEnum):
-# KBN_SH_HALL = auto()
-# KBN_SH_BATHROOM = auto()
-# KBN_SH_LIVINGROOM = auto()
-# KBN_SH_BEDROOM = auto()
-#
-# KBN_BH_2FL = auto()
-# KBN_BH_2FL_STREET = auto()
-# KBN_BH_1FL_LIVINGROOM = auto()
-# KBN_BH_1FL_BEDROOM = auto()
-# KBN_BH_1FL_BATHROOM = auto()
-#
-# KBN_NH_1FL_INV = auto()
-# KBN_NH_1FL_CENTER = auto()
-# KBN_NH_1LF_KT = auto()
-# KBN_NH_1FL_DS = auto()
-# KBN_NH_1FS_EZ = auto()
-#
-# SPB_FLAT120_CABINET = auto()
+class MqttTemphumDataPayload(MqttTemphumLegacyDataPayload):
+ FORMAT = '=ddb'
+ error: int
class MqttTempHumModule(MqttModule):
+ _legacy_payload: bool
+
def __init__(self,
sensor: Optional[BaseSensor] = None,
+ legacy_payload=False,
write_to_database=False,
*args, **kwargs):
if sensor is not None:
kwargs['tick_interval'] = 10
super().__init__(*args, **kwargs)
self._sensor = sensor
+ self._legacy_payload = legacy_payload
def on_connect(self, mqtt: MqttNode):
super().on_connect(mqtt)
@@ -69,7 +56,7 @@ class MqttTempHumModule(MqttModule):
rh = self._sensor.humidity()
except:
error = 1
- pld = MqttTemphumDataPayload(temp=temp, rh=rh, error=error)
+ pld = self._get_data_payload_cls()(temp=temp, rh=rh, error=error)
self._mqtt_node_ref.publish(DATA_TOPIC, pld.pack())
def handle_payload(self,
@@ -77,6 +64,10 @@ class MqttTempHumModule(MqttModule):
topic: str,
payload: bytes) -> Optional[MqttPayload]:
if topic == DATA_TOPIC:
- message = MqttTemphumDataPayload.unpack(payload)
+ message = self._get_data_payload_cls().unpack(payload)
self._logger.debug(message)
return message
+
+ def _get_data_payload_cls(self):
+ return MqttTemphumLegacyDataPayload if self._legacy_payload else MqttTemphumDataPayload
+
diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py
index 22bba86..2b06600 100644
--- a/include/py/homekit/util.py
+++ b/include/py/homekit/util.py
@@ -9,6 +9,8 @@ import logging
import string
import random
import re
+import os
+import ipaddress
from enum import Enum
from datetime import datetime
@@ -36,6 +38,14 @@ def validate_ipv4_or_hostname(address: str, raise_exception: bool = False) -> bo
return False
+def validate_ipv4(address: str) -> bool:
+ try:
+ ipaddress.IPv6Address(address)
+ return True
+ except ipaddress.AddressValueError:
+ return False
+
+
def validate_mac_address(mac_address: str) -> bool:
mac_pattern = r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$'
if re.match(mac_pattern, mac_address):
@@ -52,17 +62,21 @@ class Addr:
self.host = host
self.port = port
- @staticmethod
- def fromstring(addr: str) -> Addr:
- colons = addr.count(':')
- if colons != 1:
- raise ValueError('invalid host:port format')
-
- if not colons:
- host = addr
- port = None
+ @classmethod
+ def fromstring(cls, addr: str, port_required=True) -> Addr:
+ if port_required:
+ colons = addr.count(':')
+ if colons != 1:
+ raise ValueError('invalid host:port format')
+
+ if not colons:
+ host = addr
+ port = None
+ else:
+ host, port = addr.split(':')
else:
- host, port = addr.split(':')
+ port = None
+ host = addr
validate_ipv4_or_hostname(host, raise_exception=True)
@@ -73,12 +87,19 @@ class Addr:
return Addr(host, port)
+ @classmethod
+ def fromipstring(cls, addr: str) -> Addr:
+ return cls.fromstring(addr, port_required=False)
+
def __str__(self):
buf = self.host
if self.port is not None:
buf += ':'+str(self.port)
return buf
+ def __repr__(self):
+ return self.__str__()
+
def __iter__(self):
yield self.host
yield self.port
@@ -243,6 +264,24 @@ def filesize_fmt(num, suffix="B") -> str:
return f"{num:.1f} Yi{suffix}"
+def seconds_to_human_readable_string(seconds: int) -> str:
+ days, remainder = divmod(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())
@@ -252,4 +291,10 @@ def next_tick_gen(freq):
t = time.time()
while True:
t += freq
- yield max(t - time.time(), 0) \ No newline at end of file
+ yield max(t - time.time(), 0)
+
+
+def homekit_path(*args) -> str:
+ return os.path.realpath(
+ os.path.join(os.path.dirname(__file__), '..', '..', '..', *args)
+ )
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/classes/GPIORelaydClient.php b/localwebsite/classes/GPIORelaydClient.php
deleted file mode 100644
index 89c8dc9..0000000
--- a/localwebsite/classes/GPIORelaydClient.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-class GPIORelaydClient extends MySimpleSocketClient {
-
- const STATUS_ON = 'on';
- const STATUS_OFF = 'off';
-
- public function setStatus(string $status) {
- $this->send($status);
- return $this->recv();
- }
-
- public function getStatus() {
- $this->send('get');
- return $this->recv();
- }
-
-} \ No newline at end of file
diff --git a/localwebsite/classes/InverterdClient.php b/localwebsite/classes/InverterdClient.php
deleted file mode 100644
index b68b784..0000000
--- a/localwebsite/classes/InverterdClient.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-class InverterdClient extends MySimpleSocketClient {
-
- /**
- * @throws Exception
- */
- public function setProtocol(int $v): string
- {
- $this->send("v $v");
- return $this->recv();
- }
-
- /**
- * @throws Exception
- */
- public function setFormat(string $fmt): string
- {
- $this->send("format $fmt");
- return $this->recv();
- }
-
- /**
- * @throws Exception
- */
- public function exec(string $command, array $arguments = []): string
- {
- $buf = "exec $command";
- if (!empty($arguments)) {
- foreach ($arguments as $arg)
- $buf .= " $arg";
- }
- $this->send($buf);
- return $this->recv();
- }
-
- /**
- * @throws Exception
- */
- public function recv()
- {
- $recv_buf = '';
- $buf = '';
-
- while (true) {
- $result = socket_recv($this->sock, $recv_buf, 1024, 0);
- if ($result === false)
- throw new Exception(__METHOD__ . ": socket_recv() failed: " . $this->getSocketError());
-
- // peer disconnected
- if ($result === 0)
- break;
-
- $buf .= $recv_buf;
- if (endsWith($buf, "\r\n\r\n"))
- break;
- }
-
- $response = explode("\r\n", $buf);
- $status = array_shift($response);
- if (!in_array($status, ['ok', 'err']))
- throw new Exception(__METHOD__.': unexpected status ('.$status.')');
- if ($status == 'err')
- throw new Exception(empty($response) ? 'unknown inverterd error' : $response[0]);
-
- return trim(implode("\r\n", $response));
- }
-
-} \ No newline at end of file
diff --git a/localwebsite/handlers/InverterHandler.php b/localwebsite/handlers/InverterHandler.php
deleted file mode 100644
index 5fa269f..0000000
--- a/localwebsite/handlers/InverterHandler.php
+++ /dev/null
@@ -1,104 +0,0 @@
-<?php
-
-class InverterHandler extends RequestHandler
-{
-
- public function __construct() {
- parent::__construct();
- $this->tpl->add_static('inverter.js');
- }
-
- public function GET_status_page() {
- $inv = $this->getClient();
-
- $status = jsonDecode($inv->exec('get-status'))['data'];
- $rated = jsonDecode($inv->exec('get-rated'))['data'];
-
- $this->tpl->set([
- 'status' => $status,
- 'rated' => $rated,
- 'html' => $this->renderStatusHtml($status, $rated)
- ]);
- $this->tpl->set_title('Инвертор');
- $this->tpl->render_page('inverter_page.twig');
- }
-
- public function GET_set_osp() {
- list($osp) = $this->input('e:value(=sub|sbu)');
- $inv = $this->getClient();
- try {
- $inv->exec('set-output-source-priority', [strtoupper($osp)]);
- } catch (Exception $e) {
- die('Ошибка: '.jsonDecode($e->getMessage())['message']);
- }
- redirect('/inverter/');
- }
-
- public function GET_status_ajax() {
- $inv = $this->getClient();
- $status = jsonDecode($inv->exec('get-status'))['data'];
- $rated = jsonDecode($inv->exec('get-rated'))['data'];
- ajax_ok(['html' => $this->renderStatusHtml($status, $rated)]);
- }
-
- protected function renderStatusHtml(array $status, array $rated) {
- $power_direction = strtolower($status['battery_power_direction']);
- $power_direction = preg_replace('/ge$/', 'ging', $power_direction);
-
- $charging_rate = '';
- if ($power_direction == 'charging')
- $charging_rate = sprintf(' @ %s %s',
- $status['battery_charge_current']['value'],
- $status['battery_charge_current']['unit']);
- else if ($power_direction == 'discharging')
- $charging_rate = sprintf(' @ %s %s',
- $status['battery_discharge_current']['value'],
- $status['battery_discharge_current']['unit']);
-
- $html = sprintf('<b>Battery:</b> %s %s',
- $status['battery_voltage']['value'],
- $status['battery_voltage']['unit']);
- $html .= sprintf(' (%s%s, ',
- $status['battery_capacity']['value'],
- $status['battery_capacity']['unit']);
- $html .= sprintf('%s%s)',
- $power_direction,
- $charging_rate);
-
- $html .= "\n".sprintf('<b>Load:</b> %s %s',
- $status['ac_output_active_power']['value'],
- $status['ac_output_active_power']['unit']);
- $html .= sprintf(' (%s%%)',
- $status['output_load_percent']['value']);
-
- if ($status['pv1_input_power']['value'] > 0)
- $html .= "\n".sprintf('<b>Input power:</b> %s %s',
- $status['pv1_input_power']['value'],
- $status['pv1_input_power']['unit']);
-
- if ($status['grid_voltage']['value'] > 0 or $status['grid_freq']['value'] > 0) {
- $html .= "\n".sprintf('<b>AC input:</b> %s %s',
- $status['grid_voltage']['value'],
- $status['grid_voltage']['unit']);
- $html .= sprintf(', %s %s',
- $status['grid_freq']['value'],
- $status['grid_freq']['unit']);
- }
-
- $html .= "\n".sprintf('<b>Priority:</b> %s',
- $rated['output_source_priority']);
-
- return nl2br($html);
- }
-
- protected function getClient(): InverterdClient {
- global $config;
- if (isset($_GET['alt']) && $_GET['alt'] == 1)
- $config['inverterd_host'] = '192.168.5.223';
- $inv = new InverterdClient($config['inverterd_host'], $config['inverterd_port']);
- $inv->setFormat('json');
- return $inv;
- }
-
-
-}
diff --git a/localwebsite/handlers/MiscHandler.php b/localwebsite/handlers/MiscHandler.php
index 4c5a25e..efaca22 100644
--- a/localwebsite/handlers/MiscHandler.php
+++ b/localwebsite/handlers/MiscHandler.php
@@ -3,17 +3,6 @@
class MiscHandler extends RequestHandler
{
- public function GET_main() {
- global $config;
- $this->tpl->set_title('Главная');
- $this->tpl->set([
- 'grafana_sensors_url' => $config['grafana_sensors_url'],
- 'grafana_inverter_url' => $config['grafana_inverter_url'],
- 'cameras' => $config['cam_list']['labels']
- ]);
- $this->tpl->render_page('index.twig');
- }
-
public function GET_sensors_page() {
global $config;
@@ -30,29 +19,6 @@ class MiscHandler extends RequestHandler
$this->tpl->render_page('sensors.twig');
}
- public function GET_pump_page() {
- global $config;
-
- if (isset($_GET['alt']) && $_GET['alt'] == 1)
- $config['pump_host'] = '192.168.5.223';
-
- list($set) = $this->input('set');
- $client = new GPIORelaydClient($config['pump_host'], $config['pump_port']);
-
- if ($set == GPIORelaydClient::STATUS_ON || $set == GPIORelaydClient::STATUS_OFF) {
- $client->setStatus($set);
- redirect('/pump/');
- }
-
- $status = $client->getStatus();
-
- $this->tpl->set([
- 'status' => $status
- ]);
- $this->tpl->set_title('Насос');
- $this->tpl->render_page('pump.twig');
- }
-
public function GET_cams() {
global $config;
@@ -160,12 +126,4 @@ class MiscHandler extends RequestHandler
}
}
- public function GET_debug() {
- print_r($_SERVER);
- }
-
- public function GET_phpinfo() {
- phpinfo();
- }
-
}
diff --git a/localwebsite/handlers/ModemHandler.php b/localwebsite/handlers/ModemHandler.php
index 23e4c9a..94ad75b 100644
--- a/localwebsite/handlers/ModemHandler.php
+++ b/localwebsite/handlers/ModemHandler.php
@@ -7,71 +7,6 @@ use libphonenumber\PhoneNumberUtil;
class ModemHandler extends RequestHandler
{
- public function __construct()
- {
- parent::__construct();
- $this->tpl->add_static('modem.js');
- }
-
- 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;
@@ -160,111 +95,6 @@ class ModemHandler extends RequestHandler
$this->tpl->render_page('routing_dhcp_page.twig');
}
- public function GET_sms() {
- global $config;
-
- list($selected, $is_outbox, $error, $sent) = $this->input('modem, b:outbox, error, b:sent');
- if (!$selected)
- $selected = array_key_first($config['modems']);
-
- $cfg = $config['modems'][$selected];
- $e3372 = new E3372($cfg['ip'], $cfg['legacy_token_auth']);
- $messages = $e3372->getSMSList(1, 20, $is_outbox);
-
- $this->tpl->set([
- 'modems_list' => array_keys($config['modems']),
- 'modems' => $config['modems'],
- 'selected_modem' => $selected,
- 'messages' => $messages,
- 'is_outbox' => $is_outbox,
- 'error' => $error,
- 'is_sent' => $sent
- ]);
-
- $direction = $is_outbox ? 'исходящие' : 'входящие';
- $this->tpl->set_title('SMS-сообщения ('.$direction.', '.$selected.')');
- $this->tpl->render_page('sms_page.twig');
- }
-
- public function POST_sms() {
- global $config;
-
- list($selected, $is_outbox, $phone, $text) = $this->input('modem, b:outbox, phone, text');
- if (!$selected)
- $selected = array_key_first($config['modems']);
-
- $return_url = '/sms/?modem='.$selected;
- if ($is_outbox)
- $return_url .= '&outbox=1';
-
- $go_back = function(?string $error = null) use ($return_url) {
- if (!is_null($error))
- $return_url .= '&error='.urlencode($error);
- else
- $return_url .= '&sent=1';
- redirect($return_url);
- };
-
- $phone = preg_replace('/\s+/', '', $phone);
-
- // при отправке смс на короткие номера не надо использовать libphonenumber и вот это вот всё
- if (strlen($phone) > 4) {
- $country = null;
- if (!startsWith($phone, '+'))
- $country = 'RU';
-
- $phoneUtil = PhoneNumberUtil::getInstance();
- try {
- $number = $phoneUtil->parse($phone, $country);
- } catch (NumberParseException $e) {
- debugError(__METHOD__.': failed to parse number '.$phone.': '.$e->getMessage());
- $go_back('Неверный номер ('.$e->getMessage().')');
- return;
- }
-
- if (!$phoneUtil->isValidNumber($number)) {
- $go_back('Неверный номер');
- return;
- }
-
- $phone = $phoneUtil->format($number, PhoneNumberFormat::E164);
- }
-
- $cfg = $config['modems'][$selected];
- $e3372 = new E3372($cfg['ip'], $cfg['legacy_token_auth']);
-
- $result = $e3372->sendSMS($phone, $text);
- debugLog($result);
-
- $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/htdocs/assets/inverter.js b/localwebsite/htdocs/assets/inverter.js
deleted file mode 100644
index 72d985c..0000000
--- a/localwebsite/htdocs/assets/inverter.js
+++ /dev/null
@@ -1,15 +0,0 @@
-var Inverter = {
- poll: function () {
- setInterval(this._tick, 1000);
- },
-
- _tick: function() {
- ajax.get('/inverter/status.ajax')
- .then(({response}) => {
- if (response) {
- var el = document.getElementById('inverter_status');
- el.innerHTML = response.html;
- }
- });
- }
-}; \ No newline at end of file
diff --git a/localwebsite/htdocs/assets/modem.js b/localwebsite/htdocs/assets/modem.js
deleted file mode 100644
index 9fdb91d..0000000
--- a/localwebsite/htdocs/assets/modem.js
+++ /dev/null
@@ -1,29 +0,0 @@
-var ModemStatus = {
- _modems: [],
-
- init: function(modems) {
- for (var i = 0; i < modems.length; i++) {
- var modem = modems[i];
- this._modems.push(new ModemStatusUpdater(modem));
- }
- }
-};
-
-
-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/localwebsite/htdocs/index.php b/localwebsite/htdocs/index.php
index d6034e6..cd32132 100644
--- a/localwebsite/htdocs/index.php
+++ b/localwebsite/htdocs/index.php
@@ -4,11 +4,6 @@ require_once __DIR__.'/../init.php';
$router = new router;
-// modem
-$router->add('modem/', 'Modem status_page');
-$router->add('modem/verbose/', 'Modem verbose_page');
-$router->add('modem/get.ajax', 'Modem status_get_ajax');
-
$router->add('routing/', 'Modem routing_smallhome_page');
$router->add('routing/switch-small-home/', 'Modem routing_smallhome_switch');
$router->add('routing/{ipsets,dhcp}/', 'Modem routing_${1}_page');
@@ -18,15 +13,11 @@ $router->add('sms/', 'Modem sms');
// $router->add('modem/set.ajax', 'Modem ctl_set_ajax');
// inverter
-$router->add('inverter/', 'Inverter status_page');
$router->add('inverter/set-osp/', 'Inverter set_osp');
-$router->add('inverter/status.ajax', 'Inverter status_ajax');
// misc
$router->add('/', 'Misc main');
$router->add('sensors/', 'Misc sensors_page');
-$router->add('pump/', 'Misc pump_page');
-$router->add('phpinfo/', 'Misc phpinfo');
$router->add('cams/', 'Misc cams');
$router->add('cams/([\d,]+)/', 'Misc cams id=$(1)');
$router->add('cams/stat/', 'Misc cams_stat');
diff --git a/localwebsite/templates-web/inverter_page.twig b/localwebsite/templates-web/inverter_page.twig
deleted file mode 100644
index c51e1bf..0000000
--- a/localwebsite/templates-web/inverter_page.twig
+++ /dev/null
@@ -1,20 +0,0 @@
-{% include 'bc.twig' with {
- history: [
- {text: "Инвертор" }
- ]
-} %}
-
-<h6 class="text-primary">Статус</h6>
-<div id="inverter_status">
- {{ html|raw }}
-</div>
-
-<div class="pt-3">
- <a href="/inverter/set-osp/?value={{ rated.output_source_priority == 'Solar-Battery-Utility' ? 'sub' : 'sbu' }}">
- <button type="button" class="btn btn-primary">Переключить на <b>{{ rated.output_source_priority == 'Solar-Battery-Utility' ? 'Solar-Utility-Battery' : 'Solar-Battery-Utility' }}</b></button>
- </a>
-</div>
-
-{% js %}
-Inverter.poll();
-{% endjs %} \ No newline at end of file
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/requirements.txt b/requirements.txt
index 521ae41..c242f38 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,7 +6,7 @@ Werkzeug==2.3.6
uwsgi~=2.0.20
python-telegram-bot==20.3
requests==2.31.0
-aiohttp~=3.8.1
+aiohttp~=3.9.1
pytz==2023.3
PyYAML~=6.0
apscheduler==3.10.1
@@ -14,7 +14,11 @@ psutil~=5.9.1
aioshutil~=1.1
scikit-image==0.21.0
cerberus~=1.3.4
+phonenumbers~=8.13.28
# following can be installed from debian repositories
# matplotlib~=3.5.0
-Pillow==9.5.0 \ No newline at end of file
+Pillow==9.5.0
+
+jinja2~=3.1.2
+aiohttp-jinja2~=1.5.1 \ No newline at end of file
diff --git a/tasks/df_h.sh b/tasks/df_h.sh
new file mode 100644
index 0000000..eaa10fe
--- /dev/null
+++ b/tasks/df_h.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+df -h \ 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/localwebsite/htdocs/assets/app.css b/web/kbn_assets/app.css
index 3146bcf..1a4697a 100644
--- a/localwebsite/htdocs/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/localwebsite/htdocs/assets/app.js b/web/kbn_assets/app.js
index 37f1307..d575a5a 100644
--- a/localwebsite/htdocs/assets/app.js
+++ b/web/kbn_assets/app.js
@@ -316,4 +316,53 @@ window.Cameras = {
return video.canPlayType('application/vnd.apple.mpegurl');
},
};
-})(); \ No newline at end of file
+})();
+
+
+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: [],
+
+ init: function(modems) {
+ for (var i = 0; i < modems.length; i++) {
+ var modem = modems[i];
+ this._modems.push(new ModemStatusUpdater(modem));
+ }
+ }
+};
+
+
+var Inverter = {
+ poll: function () {
+ setInterval(this._tick, 1000);
+ },
+
+ _tick: function() {
+ ajax.get('/inverter.ajx')
+ .then(({response}) => {
+ if (response) {
+ var el = document.getElementById('inverter_status');
+ el.innerHTML = response.html;
+ }
+ });
+ }
+}; \ No newline at end of file
diff --git a/localwebsite/htdocs/assets/bootstrap.min.css b/web/kbn_assets/bootstrap.min.css
index edfbbb0..edfbbb0 100644
--- a/localwebsite/htdocs/assets/bootstrap.min.css
+++ b/web/kbn_assets/bootstrap.min.css
diff --git a/localwebsite/htdocs/assets/bootstrap.min.js b/web/kbn_assets/bootstrap.min.js
index aed031f..aed031f 100644
--- a/localwebsite/htdocs/assets/bootstrap.min.js
+++ b/web/kbn_assets/bootstrap.min.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106-reminified.js b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106-reminified.js
index 9a9f036..9a9f036 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106-reminified.js
+++ b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106-reminified.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106.js b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106.js
index e877ade..e877ade 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106.js
+++ b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.js
index fd26bc7..fd26bc7 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.js
+++ b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm
index de5b4f7..de5b4f7 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm
+++ b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm
Binary files differ
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func.js b/web/kbn_assets/h265webjs-dist/missile-120func.js
index fd26bc7..fd26bc7 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func.js
+++ b/web/kbn_assets/h265webjs-dist/missile-120func.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.js
index fb8f13d..fb8f13d 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.js
+++ b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm
index ee7d92a..ee7d92a 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm
+++ b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm
Binary files differ
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb.js b/web/kbn_assets/h265webjs-dist/missile-256mb.js
index fb8f13d..fb8f13d 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb.js
+++ b/web/kbn_assets/h265webjs-dist/missile-256mb.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.js
index 49ec3b6..49ec3b6 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.js
+++ b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm
index 71432e4..71432e4 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm
+++ b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm
Binary files differ
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb.js b/web/kbn_assets/h265webjs-dist/missile-512mb.js
index 49ec3b6..49ec3b6 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb.js
+++ b/web/kbn_assets/h265webjs-dist/missile-512mb.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-format.js b/web/kbn_assets/h265webjs-dist/missile-format.js
index 8f7eddf..8f7eddf 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-format.js
+++ b/web/kbn_assets/h265webjs-dist/missile-format.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-v20221120.js
index c498b84..c498b84 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.js
+++ b/web/kbn_assets/h265webjs-dist/missile-v20221120.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-v20221120.wasm
index 629ce98..629ce98 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm
+++ b/web/kbn_assets/h265webjs-dist/missile-v20221120.wasm
Binary files differ
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile.js b/web/kbn_assets/h265webjs-dist/missile.js
index c498b84..c498b84 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile.js
+++ b/web/kbn_assets/h265webjs-dist/missile.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/raw-parser.js b/web/kbn_assets/h265webjs-dist/raw-parser.js
index edc91a3..edc91a3 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/raw-parser.js
+++ b/web/kbn_assets/h265webjs-dist/raw-parser.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/worker-fetch-dist.js b/web/kbn_assets/h265webjs-dist/worker-fetch-dist.js
index e845d0e..e845d0e 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/worker-fetch-dist.js
+++ b/web/kbn_assets/h265webjs-dist/worker-fetch-dist.js
diff --git a/localwebsite/htdocs/assets/h265webjs-dist/worker-parse-dist.js b/web/kbn_assets/h265webjs-dist/worker-parse-dist.js
index 2e5d0ea..2e5d0ea 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/worker-parse-dist.js
+++ b/web/kbn_assets/h265webjs-dist/worker-parse-dist.js
diff --git a/localwebsite/htdocs/assets/hls.js b/web/kbn_assets/hls.js
index ce60c4f..ce60c4f 100644
--- a/localwebsite/htdocs/assets/hls.js
+++ b/web/kbn_assets/hls.js
diff --git a/localwebsite/htdocs/assets/polyfills.js b/web/kbn_assets/polyfills.js
index e851999..e851999 100644
--- a/localwebsite/htdocs/assets/polyfills.js
+++ b/web/kbn_assets/polyfills.js
diff --git a/web/kbn_templates/base.j2 b/web/kbn_templates/base.j2
new file mode 100644
index 0000000..e2e29e3
--- /dev/null
+++ b/web/kbn_templates/base.j2
@@ -0,0 +1,44 @@
+{% macro breadcrumbs(history) %}
+ <nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item"><a href="main.cgi">Главная</a></li>
+ {% for item in history %}
+ <li class="breadcrumb-item"{% if loop.last %} aria-current="page"{% endif %}>
+ {% if item.link %}<a href="{{ item.link }}">{% endif %}
+ {% if item.html %}
+ {% raw %}{{ item.html }}{% endraw %}
+ {% else %}
+ {{ item.text }}
+ {% endif %}
+ {% if item.link %}</a>{% endif %}
+ </li>
+ {% endfor %}
+ </ol>
+ </nav>
+{% endmacro %}
+
+<!doctype html>
+<html>
+<head>
+ <title>{{ title }}</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">
+ <script>
+ window.onerror = function(error) {
+ window.console && console.error(error);
+ }
+ </script>
+ {{ head_static | safe }}
+</head>
+<body>
+<div class="container py-3">
+
+{% block content %}{% endblock %}
+
+<script>
+{% block js %}{% endblock %}
+</script>
+
+</div>
+</body>
+</html>
diff --git a/web/kbn_templates/index.j2 b/web/kbn_templates/index.j2
new file mode 100644
index 0000000..c356326
--- /dev/null
+++ b/web/kbn_templates/index.j2
@@ -0,0 +1,39 @@
+{% extends "base.j2" %}
+
+{% block content %}
+<div class="container py-4">
+ <nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item active" aria-current="page">Главная</li>
+ </ol>
+ </nav>
+
+<!-- {% if auth_user %}-->
+<!-- <div class="mb-4 alert alert-secondary">-->
+<!-- Вы авторизованы как <b>{{ auth_user.username }}</b>. <a href="/deauth/">Выйти</a>-->
+<!-- </div>-->
+<!-- {% endif %}-->
+
+ <h6>Интернет</h6>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item"><a href="/modems.cgi">Модемы</a></li>
+ <li class="list-group-item"><a href="/routing.cgi">Маршрутизация</a></li>
+ <li class="list-group-item"><a href="/sms.cgi">SMS-сообщения</a></li>
+ </ul>
+
+ <h6 class="mt-4">Другое</h6>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item"><a href="/inverter.cgi">Инвертор</a> (<a href="{{ inverter_grafana_url }}">Grafana</a>)</li>
+ <li class="list-group-item"><a href="/pump.cgi">Насос</a></li>
+ <li class="list-group-item"><a href="/sensors.cgi">Датчики</a> (<a href="{{ sensors_grafana_url }}">Grafana</a>)</li>
+ </ul>
+
+ <h6 class="mt-4"><a href="/cams/"><b>Все камеры</b></a> (<a href="/cams/?high=1">HQ</a>)</h6>
+ <ul class="list-group list-group-flush">
+ {% for id, name in cameras %}
+ <li class="list-group-item"><a href="/cams/{{ id }}/">{{ name }}</a> (<a href="/cams/{{ id }}/?high=1">HQ</a>)</li>
+ {% endfor %}
+ <li class="list-group-item"><a href="/cams/stat/">Статистика</a></li>
+ </ul>
+</div>
+{% endblock %} \ No newline at end of file
diff --git a/web/kbn_templates/inverter.j2 b/web/kbn_templates/inverter.j2
new file mode 100644
index 0000000..26491f3
--- /dev/null
+++ b/web/kbn_templates/inverter.j2
@@ -0,0 +1,20 @@
+{% extends "base.j2" %}
+
+{% block content %}
+{{ breadcrumbs([{'text': 'Инвертор'}]) }}
+
+<h6 class="text-primary">Статус</h6>
+<div id="inverter_status">
+ {{ html|safe }}
+</div>
+
+<div class="pt-3">
+ <a href="/inverter.cgi?do=set-osp&amp;value={{ 'sub' if rated.output_source_priority == 'Solar-Battery-Utility' else 'sbu' }}">
+ <button type="button" class="btn btn-primary">Переключить на <b>{{ 'Solar-Utility-Battery' if rated.output_source_priority == 'Solar-Battery-Utility' else 'Solar-Battery-Utility' }}</b></button>
+ </a>
+</div>
+{% endblock %}
+
+{% block js %}
+Inverter.poll();
+{% endblock %} \ No newline at end of file
diff --git a/web/kbn_templates/loading.j2 b/web/kbn_templates/loading.j2
new file mode 100644
index 0000000..d064a48
--- /dev/null
+++ b/web/kbn_templates/loading.j2
@@ -0,0 +1,14 @@
+<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/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
new file mode 100644
index 0000000..06339f8
--- /dev/null
+++ b/web/kbn_templates/modems.j2
@@ -0,0 +1,16 @@
+{% extends "base.j2" %}
+
+{% block content %}
+{{ breadcrumbs([{'text': 'Модемы'}]) }}
+
+{% for modem in modems %}
+<h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ modems.getfullname(modem) }}</h6>
+<div id="modem_data_{{ modem }}">
+ {% include "loading.j2" %}
+</div>
+{% endfor %}
+{% endblock %}
+
+{% block js %}
+ModemStatus.init({{ modems.getkeys()|tojson }});
+{% endblock %}
diff --git a/localwebsite/templates-web/pump.twig b/web/kbn_templates/pump.j2
index 3bce0e2..28d5c9d 100644
--- a/localwebsite/templates-web/pump.twig
+++ b/web/kbn_templates/pump.j2
@@ -1,11 +1,10 @@
-{% include 'bc.twig' with {
- history: [
- {text: "Насос" }
- ]
-} %}
+{% extends "base.j2" %}
-<form action="/pump/" method="get">
- <input type="hidden" name="set" value="{{ status == 'on' ? 'off' : 'on' }}" />
+{% block content %}
+{{ breadcrumbs([{'text': 'Насос'}]) }}
+
+<form action="/pump.cgi" method="get">
+ <input type="hidden" name="set" value="{{ 'off' if status == 'on' else 'on' }}" />
Сейчас насос
{% if status == 'on' %}
<span class="text-success"><b>включен</b></span>.<br><br>
@@ -14,4 +13,5 @@
<span class="text-danger"><b>выключен</b></span>.<br><br>
<button type="submit" class="btn btn-primary">Включить</button>
{% endif %}
-</form> \ No newline at end of file
+</form>
+{% endblock %}
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
diff --git a/localwebsite/templates-web/sms_page.twig b/web/kbn_templates/sms.j2
index 112fa64..6de9d42 100644
--- a/localwebsite/templates-web/sms_page.twig
+++ b/web/kbn_templates/sms.j2
@@ -1,14 +1,13 @@
-{% include 'bc.twig' with {
- history: [
- {text: "SMS-сообщения" }
- ]
-} %}
+{% extends "base.j2" %}
+
+{% block content %}
+{{ breadcrumbs([{'text': 'SMS-сообщения'}]) }}
<nav>
<div class="nav nav-tabs" id="nav-tab">
- {% for modem in modems_list %}
- {% if selected_modem != modem %}<a href="/sms/?modem={{ modem }}" class="text-decoration-none">{% endif %}
- <button class="nav-link{% if modem == selected_modem %} active{% endif %}" type="button">{{ modems[modem].short_label }}</button>
+ {% for modem in modems.keys() %}
+ {% if selected_modem != modem %}<a href="/sms.cgi?id={{ modem }}" class="text-decoration-none">{% endif %}
+ <button class="nav-link{% if modem == selected_modem %} active{% endif %}" type="button">{{ modems.getshortname(modem) }}</button>
{% if selected_modem != modem %}</a>{% endif %}
{% endfor %}
</div>
@@ -20,14 +19,14 @@
<div class="alert alert-success" role="alert">
Сообщение отправлено.
</div>
-{% elseif error %}
+{% elif error %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endif %}
<div>
- <form method="post" action="/sms/">
+ <form method="post" action="/sms.cgi">
<input type="hidden" name="modem" value="{{ selected_modem }}">
<div class="form-floating mb-3">
<input type="text" name="phone" class="form-control" id="inputPhone" placeholder="+7911xxxyyzz">
@@ -46,17 +45,19 @@
<h6 class="text-primary mt-4">
Последние
{% if not is_outbox %}
- <b>входящие</b> <span class="text-black-50">|</span> <a href="/sms/?modem={{ selected_modem }}&amp;outbox=1">исходящие</a>
+ <b>входящие</b> <span class="text-black-50">|</span> <a href="/sms.cgi?id={{ selected_modem }}&amp;outbox=1">исходящие</a>
{% else %}
- <a href="/sms/?modem={{ selected_modem }}">входящие</a> <span class="text-black-50">|</span> <b>исходящие</b>
+ <a href="/sms.cgi?id={{ selected_modem }}">входящие</a> <span class="text-black-50">|</span> <b>исходящие</b>
{% endif %}
</h6>
{% for m in messages %}
<div class="mt-3">
- <b>{{ m.phone }}</b> <span class="text-secondary">({{ m.date }})</span><br/>
- {{ m.content }}
+ <b>{{ m.Phone }}</b> <span class="text-secondary">({{ m.Date }})</span><br/>
+ {{ m.Content }}
</div>
{% else %}
<span class="text-secondary">Сообщений нет.</span>
-{% endfor %} \ No newline at end of file
+{% endfor %}
+
+{% endblock %} \ No newline at end of file