diff options
-rwxr-xr-x | bin/ipcam_capture.py | 3 | ||||
-rwxr-xr-x | bin/ipcam_ntp_util.py | 199 | ||||
-rwxr-xr-x | bin/mqtt_node_util.py | 5 | ||||
-rw-r--r-- | bin/web_kbn.py | 354 | ||||
-rw-r--r-- | include/py/homekit/camera/config.py | 51 | ||||
-rw-r--r-- | include/py/homekit/camera/types.py | 32 | ||||
-rw-r--r-- | include/py/homekit/config/_configs.py | 1 | ||||
-rw-r--r-- | include/py/homekit/config/config.py | 30 | ||||
-rw-r--r-- | include/py/homekit/http/__init__.py | 4 | ||||
-rw-r--r-- | include/py/homekit/http/http.py | 11 | ||||
-rw-r--r-- | include/py/homekit/inverter/config.py | 4 | ||||
-rw-r--r-- | include/py/homekit/modem/__init__.py | 2 | ||||
-rw-r--r-- | include/py/homekit/modem/config.py | 29 | ||||
-rw-r--r-- | include/py/homekit/modem/e3372.py | 253 | ||||
-rw-r--r-- | include/py/homekit/mqtt/_config.py | 13 | ||||
-rw-r--r-- | include/py/homekit/mqtt/module/temphum.py | 39 | ||||
-rw-r--r-- | include/py/homekit/util.py | 67 | ||||
-rw-r--r-- | localwebsite/classes/E3372.php | 310 | ||||
-rw-r--r-- | localwebsite/classes/GPIORelaydClient.php | 18 | ||||
-rw-r--r-- | localwebsite/classes/InverterdClient.php | 69 | ||||
-rw-r--r-- | localwebsite/handlers/InverterHandler.php | 104 | ||||
-rw-r--r-- | localwebsite/handlers/MiscHandler.php | 42 | ||||
-rw-r--r-- | localwebsite/handlers/ModemHandler.php | 170 | ||||
-rw-r--r-- | localwebsite/htdocs/assets/inverter.js | 15 | ||||
-rw-r--r-- | localwebsite/htdocs/assets/modem.js | 29 | ||||
-rw-r--r-- | localwebsite/htdocs/index.php | 9 | ||||
-rw-r--r-- | localwebsite/templates-web/inverter_page.twig | 20 | ||||
-rw-r--r-- | localwebsite/templates-web/modem_data.twig | 14 | ||||
-rw-r--r-- | localwebsite/templates-web/modem_status_page.twig | 19 | ||||
-rw-r--r-- | localwebsite/templates-web/modem_verbose_page.twig | 15 | ||||
-rw-r--r-- | localwebsite/templates-web/spinner.twig | 14 | ||||
-rw-r--r-- | requirements.txt | 8 | ||||
-rw-r--r-- | tasks/df_h.sh | 2 | ||||
-rwxr-xr-x | test/test_modems.py | 9 | ||||
-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) | bin | 2190151 -> 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) | bin | 2108889 -> 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) | bin | 2108889 -> 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) | bin | 2108891 -> 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.j2 | 44 | ||||
-rw-r--r-- | web/kbn_templates/index.j2 | 39 | ||||
-rw-r--r-- | web/kbn_templates/inverter.j2 | 20 | ||||
-rw-r--r-- | web/kbn_templates/loading.j2 | 14 | ||||
-rw-r--r-- | web/kbn_templates/modem_data.j2 | 13 | ||||
-rw-r--r-- | web/kbn_templates/modem_verbose.j2 | 18 | ||||
-rw-r--r-- | web/kbn_templates/modems.j2 | 16 | ||||
-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 Binary files differindex de5b4f7..de5b4f7 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm +++ b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm 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 Binary files differindex ee7d92a..ee7d92a 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm +++ b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm 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 Binary files differindex 71432e4..71432e4 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm +++ b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm 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 Binary files differindex 629ce98..629ce98 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm +++ b/web/kbn_assets/h265webjs-dist/missile-v20221120.wasm 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&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 }}&outbox=1">исходящие</a> + <b>входящие</b> <span class="text-black-50">|</span> <a href="/sms.cgi?id={{ selected_modem }}&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 |