aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2024-02-18 01:35:57 +0300
committerEvgeny Zinoviev <me@ch1p.io>2024-02-18 01:35:57 +0300
commit42155370475b1f6619498ec2c43c1c7f328ce1a1 (patch)
treeaa7a708bb94362e0c79d7f3678d07883baa4ba76
parent70b4a4f044cac8052bb0af7c585572e54489ea2f (diff)
lws: wip
-rw-r--r--bin/web_kbn.py55
-rw-r--r--include/py/homekit/camera/config.py7
-rw-r--r--include/py/homekit/config/config.py16
-rw-r--r--include/py/homekit/util.py4
-rw-r--r--web/kbn_assets/app.css37
-rw-r--r--web/kbn_assets/app.js79
-rw-r--r--web/kbn_templates/index.j229
-rw-r--r--web/kbn_templates/modems.j22
8 files changed, 176 insertions, 53 deletions
diff --git a/bin/web_kbn.py b/bin/web_kbn.py
index c21269b..5b36d53 100644
--- a/bin/web_kbn.py
+++ b/bin/web_kbn.py
@@ -6,13 +6,15 @@ import json
import re
import inverterd
import phonenumbers
+import time
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.config import config, AppConfigUnit, is_development_mode, Translation
+from homekit.camera import IpcamConfig
+from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string, json_serial
from homekit.modem import E3372, ModemsConfig, MacroNetWorkType
from homekit.inverter.config import InverterdConfig
from homekit.relay.sunxi_h3_client import RelayClient
@@ -43,12 +45,16 @@ STATIC_FILES = [
def get_js_link(file, version) -> str:
+ if is_development_mode():
+ version = int(time.time())
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 is_development_mode():
+ version = int(time.time())
if version:
file += f'?version={version}'
return f'<link rel="stylesheet" type="text/css" href="{config.app_config["assets_public_path"]}/{file}">'
@@ -171,20 +177,22 @@ def get_inverter_data() -> tuple:
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=(',', ':'))
+
+ def filter_lang(key, unit):
+ strings = Translation(unit)
+ return strings.get()[key]
+
+ env.filters['tojson'] = lambda obj: json.dumps(obj, separators=(',', ':'), default=json_serial)
+ env.filters['lang'] = filter_lang
self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets'))
@@ -200,6 +208,8 @@ class WebSite(http.HTTPServer):
self.get('/sms.cgi', self.sms)
self.post('/sms.cgi', self.sms_post)
+ self.get('/cams.cgi', self.cams)
+
async def render_page(self,
req: http.Request,
template_name: str,
@@ -220,6 +230,11 @@ class WebSite(http.HTTPServer):
ctx = {}
for k in 'inverter', 'sensors':
ctx[f'{k}_grafana_url'] = config.app_config[f'{k}_grafana_url']
+
+ cc = IpcamConfig()
+ ctx['camzones'] = cc['zones'].keys()
+ ctx['allcams'] = cc.get_all_cam_names()
+
return await self.render_page(req, 'index',
title="Home web site",
context=ctx)
@@ -227,14 +242,15 @@ class WebSite(http.HTTPServer):
async def modems(self, req: http.Request):
return await self.render_page(req, 'modems',
title='Состояние модемов',
- context=dict(modems=self._modems_config))
+ context=dict(modems=ModemsConfig()))
async def modems_ajx(self, req: http.Request):
+ mc = ModemsConfig()
modem = req.query.get('id', None)
- if modem not in self._modems_config.keys():
+ if modem not in mc.keys():
raise ValueError('invalid modem id')
- modem_cfg = self._modems_config.get(modem)
+ modem_cfg = mc.get(modem)
loop = asyncio.get_event_loop()
modem_data = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg))
@@ -247,10 +263,10 @@ class WebSite(http.HTTPServer):
async def modems_verbose(self, req: http.Request):
modem = req.query.get('id', None)
- if modem not in self._modems_config.keys():
+ if modem not in ModemsConfig().keys():
raise ValueError('invalid modem id')
- modem_cfg = self._modems_config.get(modem)
+ modem_cfg = ModemsConfig().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 = [
@@ -261,23 +277,23 @@ class WebSite(http.HTTPServer):
['Dialup connection', dialup_conn]
]
- modem_name = self._modems_config.getfullname(modem)
+ modem_name = ModemsConfig().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])
+ modem = req.query.get('id', list(ModemsConfig().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])
+ cl = get_modem_client(ModemsConfig()[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,
+ modems=ModemsConfig(),
selected_modem=modem,
is_outbox=is_outbox,
error=error,
@@ -286,7 +302,7 @@ class WebSite(http.HTTPServer):
))
async def sms_post(self, req: http.Request):
- modem = req.query.get('id', list(self._modems_config.keys())[0])
+ modem = req.query.get('id', list(ModemsConfig().keys())[0])
is_outbox = int(req.query.get('outbox', 0)) == 1
fd = await req.post()
@@ -305,7 +321,7 @@ class WebSite(http.HTTPServer):
raise HTTPFound(f'{return_url}&error=Неверный+номер')
phone = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
- cl = get_modem_client(self._modems_config[modem])
+ cl = get_modem_client(ModemsConfig()[modem])
cl.sms_send(phone, text)
raise HTTPFound(return_url)
@@ -346,6 +362,9 @@ class WebSite(http.HTTPServer):
title='Насос',
context=dict(status=status))
+ async def cams(self, req: http.Request):
+ pass
+
if __name__ == '__main__':
config.load_app(WebKbnConfig)
diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py
index 9685cab..bcd5d07 100644
--- a/include/py/homekit/camera/config.py
+++ b/include/py/homekit/camera/config.py
@@ -44,7 +44,7 @@ class IpcamConfig(ConfigUnit):
}
}
},
- 'areas': {
+ 'zones': {
'type': 'dict',
'keysrules': {'type': 'string'},
'valuesrules': {
@@ -114,11 +114,14 @@ class IpcamConfig(ConfigUnit):
# FIXME
def get_all_cam_names(self,
filter_by_server: Optional[str] = None,
- filter_by_disk: Optional[int] = None) -> list[int]:
+ filter_by_disk: Optional[int] = None,
+ only_enabled=True) -> list[int]:
cams = []
if filter_by_server is not None and filter_by_server not in _lbc:
raise ValueError(f'invalid filter_by_server: {filter_by_server} not found in {_lbc.__class__.__name__}')
for cam, params in self['cameras'].items():
+ if only_enabled and not self.is_camera_enabled(cam):
+ continue
if filter_by_server is None or params['server'] == filter_by_server:
if filter_by_disk is None or params['disk'] == filter_by_disk:
cams.append(int(cam))
diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py
index 40ac211..3aa0e04 100644
--- a/include/py/homekit/config/config.py
+++ b/include/py/homekit/config/config.py
@@ -266,14 +266,26 @@ class TranslationUnit(BaseConfigUnit):
pass
+TranslationInstances = {}
+
+
class Translation:
LANGUAGES = ('en', 'ru')
DEFAULT_LANGUAGE = 'ru'
_langs: dict[str, TranslationUnit]
+ # def __init_subclass__(cls, **kwargs):
+ # super().__init_subclass__(**kwargs)
+ # cls._instance = None
+
+ def __new__(cls, *args, **kwargs):
+ unit = args[0]
+ if unit not in TranslationInstances:
+ TranslationInstances[unit] = super(Translation, cls).__new__(cls)
+ return TranslationInstances[unit]
+
def __init__(self, name: str):
- super().__init__()
self._langs = {}
for lang in self.LANGUAGES:
for dirname in CONFIG_DIRECTORIES:
@@ -289,7 +301,7 @@ class Translation:
if len(diff) > 0:
raise RuntimeError(f'{name}: translation units have difference in keys: ' + ', '.join(diff))
- def get(self, lang: str) -> TranslationUnit:
+ def get(self, lang: str = DEFAULT_LANGUAGE) -> TranslationUnit:
return self._langs[lang]
diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py
index 7732d3b..c686f29 100644
--- a/include/py/homekit/util.py
+++ b/include/py/homekit/util.py
@@ -12,7 +12,7 @@ import re
import os
import ipaddress
-from collections import namedtuple
+from collections.abc import KeysView
from enum import Enum
from datetime import datetime
from typing import Optional, List
@@ -119,6 +119,8 @@ def json_serial(obj):
return obj.timestamp()
if isinstance(obj, Enum):
return obj.value
+ if isinstance(obj, KeysView):
+ return list(obj)
raise TypeError("Type %s not serializable" % type(obj))
diff --git a/web/kbn_assets/app.css b/web/kbn_assets/app.css
index 1a4697a..ee0aa3e 100644
--- a/web/kbn_assets/app.css
+++ b/web/kbn_assets/app.css
@@ -173,3 +173,40 @@
.camfeeds.is_mobile > .video-container {
max-width: 100%;
}
+
+
+/* index page */
+.camzones {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 5px -5px 0;
+}
+.camzones::after {
+ content: "";
+ flex: 0 0 50%;
+}
+a.camzone {
+ display: block;
+ text-decoration: none;
+ color: var(--bs-dark);
+ flex: 0 0 calc(50% - 10px);
+ height: 100px;
+ box-sizing: border-box;
+ padding: 10px;
+ margin: 5px;
+ /*border: 1px solid #ccc;*/
+ background: #f0f2f4;
+ border-radius: 4px;
+ word-wrap: break-word;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ position: relative;
+}
+.camzone_text {
+ position: absolute;
+ bottom: 5px;
+ left: 8px;
+ right: 8px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+} \ No newline at end of file
diff --git a/web/kbn_assets/app.js b/web/kbn_assets/app.js
index d575a5a..6081681 100644
--- a/web/kbn_assets/app.js
+++ b/web/kbn_assets/app.js
@@ -1,7 +1,6 @@
-(function() {
var RE_WHITESPACE = /[\t\r\n\f]/g
-window.ajax = {
+var ajax = {
get: function(url, data) {
if (typeof data == 'object') {
var index = 0;
@@ -38,35 +37,37 @@ window.ajax = {
}
};
-window.extend = function(a, b) {
+function extend(a, b) {
return Object.assign(a, b);
}
-window.ge = function(id) {
+function ge(id) {
return document.getElementById(id);
}
-var ua = navigator.userAgent.toLowerCase();
-window.browserInfo = {
- version: (ua.match(/.+(?:me|ox|on|rv|it|ra|ie)[\/: ]([\d.]+)/) || [0,'0'])[1],
- //opera: /opera/i.test(ua),
- msie: (/msie/i.test(ua) && !/opera/i.test(ua)) || /trident/i.test(ua),
- mozilla: /firefox/i.test(ua),
- android: /android/i.test(ua),
- mac: /mac/i.test(ua),
- samsungBrowser: /samsungbrowser/i.test(ua),
- chrome: /chrome/i.test(ua),
- safari: /safari/i.test(ua),
- mobile: /iphone|ipod|ipad|opera mini|opera mobi|iemobile|android/i.test(ua),
- operaMini: /opera mini/i.test(ua),
- ios: /iphone|ipod|ipad|watchos/i.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1),
-};
+(function() {
+ var ua = navigator.userAgent.toLowerCase();
+ window.browserInfo = {
+ version: (ua.match(/.+(?:me|ox|on|rv|it|ra|ie)[\/: ]([\d.]+)/) || [0,'0'])[1],
+ //opera: /opera/i.test(ua),
+ msie: (/msie/i.test(ua) && !/opera/i.test(ua)) || /trident/i.test(ua),
+ mozilla: /firefox/i.test(ua),
+ android: /android/i.test(ua),
+ mac: /mac/i.test(ua),
+ samsungBrowser: /samsungbrowser/i.test(ua),
+ chrome: /chrome/i.test(ua),
+ safari: /safari/i.test(ua),
+ mobile: /iphone|ipod|ipad|opera mini|opera mobi|iemobile|android/i.test(ua),
+ operaMini: /opera mini/i.test(ua),
+ ios: /iphone|ipod|ipad|watchos/i.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1),
+ };
+})();
-window.isTouchDevice = function() {
+function isTouchDevice() {
return 'ontouchstart' in window || navigator.msMaxTouchPoints;
}
-window.hasClass = function(el, name) {
+function hasClass(el, name) {
if (!el)
throw new Error('hasClass: invalid element')
@@ -80,7 +81,7 @@ window.hasClass = function(el, name) {
}
}
-window.addClass = function(el, name) {
+function addClass(el, name) {
if (!hasClass(el, name)) {
el.className = (el.className ? el.className + ' ' : '') + name;
return true
@@ -88,6 +89,39 @@ window.addClass = function(el, name) {
return false
}
+function removeClass(el, name) {
+ if (!el)
+ throw new Error('removeClass: invalid element')
+
+ if (Array.isArray(name)) {
+ for (var i = 0; i < name.length; i++)
+ removeClass(el, name[i]);
+ return;
+ }
+ el.className = ((el.className || '').replace((new RegExp('(\\s|^)' + name + '(\\s|$)')), ' ')).trim()
+}
+
+function indexInit() {
+ var blocks = ['zones', 'all'];
+ for (var i = 0; i < blocks.length; i++) {
+ var button = ge('cam_'+blocks[i]+'_btn');
+ button.addEventListener('click', function(e) {
+ var selected = e.target.getAttribute('data-id');
+ for (var j = 0; j < blocks.length; j++) {
+ var button = ge('cam_'+blocks[j]+'_btn');
+ var content = ge('cam_'+blocks[j]);
+ if (blocks[j] === selected) {
+ addClass(button, 'active');
+ content.style.display = '';
+ } else {
+ removeClass(button, 'active');
+ content.style.display = 'none';
+ }
+ }
+ });
+ }
+}
+
window.Cameras = {
hlsOptions: null,
h265webjsOptions: null,
@@ -316,7 +350,6 @@ window.Cameras = {
return video.canPlayType('application/vnd.apple.mpegurl');
},
};
-})();
class ModemStatusUpdater {
diff --git a/web/kbn_templates/index.j2 b/web/kbn_templates/index.j2
index c356326..cdf3026 100644
--- a/web/kbn_templates/index.j2
+++ b/web/kbn_templates/index.j2
@@ -28,12 +28,29 @@
<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>
+ <nav class="mt-4">
+ <div class="nav nav-tabs" id="nav-tab">
+ <button class="nav-link active" type="button" id="cam_zones_btn" data-id="zones">По зонам</button>
+ <button class="nav-link" type="button" id="cam_all_btn" data-id="all">Все камеры</button>
+ </div>
+ </nav>
+
+ <div class="camzones" id="cam_zones">
+ {% for zone in camzones %}
+ <a href="/cams.cgi?zone={{ zone }}" class="camzone">
+ <div class="camzone_text">{{ zone|lang('ipcam_zones') }}</div>
+ </a>
{% endfor %}
- <li class="list-group-item"><a href="/cams/stat/">Статистика</a></li>
+ </div>
+ <ul class="list-group list-group-flush" id="cam_all" style="display: none">
+ {% for id in allcams %}
+ <li class="list-group-item"><a href="/cams.cgi?id={{ id }}">{{ id|lang('ipcam') }}</a></li>
+ {% endfor %}
+{# <li class="list-group-item"><a href="/cams/stat/">Статистика</a></li>#}
</ul>
</div>
-{% endblock %} \ No newline at end of file
+{% endblock %}
+
+{% block js %}
+indexInit();
+{% endblock %}
diff --git a/web/kbn_templates/modems.j2 b/web/kbn_templates/modems.j2
index 06339f8..9defec3 100644
--- a/web/kbn_templates/modems.j2
+++ b/web/kbn_templates/modems.j2
@@ -12,5 +12,5 @@
{% endblock %}
{% block js %}
-ModemStatus.init({{ modems.getkeys()|tojson }});
+ModemStatus.init({{ modems.keys()|tojson }});
{% endblock %}