inverter page

This commit is contained in:
Evgeny Sorokin 2024-01-16 03:31:55 +03:00
parent de56aa3ae9
commit 8a89dd77be
9 changed files with 141 additions and 159 deletions

View File

@ -3,16 +3,17 @@ import asyncio
import jinja2
import aiohttp_jinja2
import json
import os
import re
import inverterd
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 aiohttp import web
from homekit.inverter.config import InverterdConfig
from homekit import http
@ -90,6 +91,69 @@ def get_modem_data(modem_cfg: dict, get_raw=False) -> Union[dict, tuple]:
}
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
@ -108,10 +172,14 @@ class WebSite(http.HTTPServer):
self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets'))
self.get('/main.cgi', self.get_index)
self.get('/modems.cgi', self.get_modems)
self.get('/modems/info.ajx', self.get_modems_ajax)
self.get('/modems/verbose.cgi', self.get_modems_verbose)
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)
async def render_page(self,
req: http.Request,
@ -129,16 +197,16 @@ class WebSite(http.HTTPServer):
response = aiohttp_jinja2.render_template(template_name+'.j2', req, context=context)
return response
async def get_index(self, req: http.Request):
async def index(self, req: http.Request):
return await self.render_page(req, 'index',
title="Home web site")
async def get_modems(self, req: http.Request):
async def modems(self, req: http.Request):
return await self.render_page(req, 'modems',
title='Состояние модемов',
context=dict(modems=self._modems_config))
async def get_modems_ajax(self, req: http.Request):
async def modems_ajx(self, req: http.Request):
modem = req.query.get('id', None)
if modem not in self._modems_config.getkeys():
raise ValueError('invalid modem id')
@ -154,7 +222,7 @@ class WebSite(http.HTTPServer):
return self.ok({'html': html})
async def get_modems_verbose(self, req: http.Request):
async def modems_verbose(self, req: http.Request):
modem = req.query.get('id', None)
if modem not in self._modems_config.getkeys():
raise ValueError('invalid modem id')
@ -175,6 +243,26 @@ class WebSite(http.HTTPServer):
title=f'Подробная информация о модеме "{modem_name}"',
context=dict(data=data, modem_name=modem_name))
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})
if __name__ == '__main__':
config.load_app(WebKbnConfig)

View File

@ -3,7 +3,7 @@ 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
@ -21,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 = {

View File

@ -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),
}

View File

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

View File

@ -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,9 +13,7 @@ $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');

View File

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

View File

@ -349,3 +349,20 @@ var ModemStatus = {
}
}
};
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;
}
});
}
};

View File

@ -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;
}
});
}
};

View File

@ -0,0 +1,20 @@
{% extends "base.j2" %}
{% block content %}
{{ breadcrumbs([{'text': 'Инвертор'}]) }}
<h6 class="text-primary">Статус</h6>
<div id="inverter_status">
{{ html|safe }}
</div>
<div class="pt-3">
<a href="/inverter.cgi?do=set-osp&amp;value={{ 'sub' if rated.output_source_priority == 'Solar-Battery-Utility' else 'sbu' }}">
<button type="button" class="btn btn-primary">Переключить на <b>{{ 'Solar-Utility-Battery' if rated.output_source_priority == 'Solar-Battery-Utility' else 'Solar-Battery-Utility' }}</b></button>
</a>
</div>
{% endblock %}
{% block js %}
Inverter.poll();
{% endblock %}