This commit is contained in:
Evgeny Zinoviev 2024-01-10 03:20:10 +03:00
parent 54ddea4614
commit 05c5d18f76
12 changed files with 191 additions and 82 deletions

View File

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

View File

@ -75,27 +75,35 @@ class WebSite(http.HTTPServer):
self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets'))
self.get('/', self.get_index)
self.get('/modems', self.get_modems)
self.get('/main.cgi', self.get_index)
self.get('/modems.cgi', self.get_modems)
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(),
'title': 'this is title'
'head_static': get_head_static()
}
response = aiohttp_jinja2.render_template('index.html', req, context=context)
if title is not None:
context['title'] = title
response = aiohttp_jinja2.render_template(template_name+'.j2', req, context=context)
return response
async def get_index(self, req: http.Request):
return await self.render_page(req)
return await self.render_page(req, 'index',
title="Home web site")
async def get_modems(self, req: http.Request):
pass
mc = ModemsConfig()
print(mc)
return await self.render_page(req, 'modems',
title='Состояние модемов',
context=dict(modems=ModemsConfig()))
if __name__ == '__main__':

View File

@ -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]
@ -123,10 +126,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 +161,7 @@ class ConfigUnit(BaseConfigUnit):
pass
v = MyValidator()
need_document = False
if rst == RootSchemaType.DICT:
normalized = v.validated({'document': self._data},
@ -165,16 +169,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 +244,8 @@ class TranslationUnit(BaseConfigUnit):
class Translation:
LANGUAGES = ('en', 'ru')
DEFAULT_LANGUAGE = 'ru'
_langs: dict[str, TranslationUnit]
def __init__(self, name: str):

View File

@ -1,5 +1,29 @@
from ..config import ConfigUnit
from ..config import ConfigUnit, Translation
from typing import Optional
class ModemsConfig(ConfigUnit):
pass
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']

View File

@ -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': {
@ -109,7 +110,12 @@ class MqttNodesConfig(ConfigUnit):
'legacy_topics': {'type': 'boolean'}
}
},
'password': {'type': 'string'}
'password': {'type': 'string'},
'defines': {
'type': 'dict',
'keysrules': {'type': 'string'},
'valuesrules': {'type': ['string', 'integer']}
}
}
}
}
@ -163,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

View File

@ -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
class MqttTemphumDataPayload(MqttTemphumLegacyDataPayload):
FORMAT = '=ddb'
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 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

View File

@ -53,17 +53,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')
@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
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)
@ -74,12 +78,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

View File

@ -1,25 +0,0 @@
<!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 %}
{% if js %}
<script>{{ js|raw }}</script>
{% endif %}
</div>
</body>
</html>

44
web/kbn_templates/base.j2 Normal file
View File

@ -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 %}
{% if js %}
<script>{{ js|raw }}</script>
{% endif %}
</div>
</body>
</html>

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "base.j2" %}
{% block content %}
<div class="container py-4">
@ -16,16 +16,16 @@
<h6>Интернет</h6>
<ul class="list-group list-group-flush">
<li class="list-group-item"><a href="/modem/">Модемы</a></li>
<li class="list-group-item"><a href="/routing/">Маршрутизация</a></li>
<li class="list-group-item"><a href="/sms/">SMS-сообщения</a></li>
<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/">Инвертор</a> (<a href="{{ grafana_inverter_url }}">Grafana</a>)</li>
<li class="list-group-item"><a href="/pump/">Насос</a></li>
<li class="list-group-item"><a href="/sensors/">Датчики</a> (<a href="{{ grafana_sensors_url }}">Grafana</a>)</li>
<li class="list-group-item"><a href="/inverter.cgi">Инвертор</a> (<a href="{{ grafana_inverter_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="{{ grafana_sensors_url }}">Grafana</a>)</li>
</ul>
<h6 class="mt-4"><a href="/cams/"><b>Все камеры</b></a> (<a href="/cams/?high=1">HQ</a>)</h6>

View File

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

View File

@ -0,0 +1,12 @@
{% 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 %}