Changes in user interface

This commit is contained in:
Gunnar Skjold
2021-01-14 16:19:00 +01:00
parent 837c3cf802
commit 037bac24de
11 changed files with 339 additions and 179 deletions

View File

@@ -287,6 +287,30 @@ void AmsData::extractFromOmnipower(HanReader& hanReader, int listSize) {
}
void AmsData::apply(AmsData& other) {
if(other.getListType() < 3) {
unsigned long ms = this->lastUpdateMillis > other.getLastUpdateMillis() ? 0 : other.getLastUpdateMillis() - this->lastUpdateMillis;
if(ms > 0) {
if(other.getActiveImportPower() > 0)
activeImportCounter += (((double) ms) * other.getActiveImportPower()) / 3600000000;
counterEstimated = true;
}
if(other.getListType() > 1) {
unsigned long ms2 = this->lastList2UpdateMillis > other.getLastUpdateMillis() ? 0 : other.getLastUpdateMillis() - this->lastList2UpdateMillis;
if(ms2 > 0) {
// Not sure why, but I cannot make these numbers correct. It seems to be double of what it should, so dividing it by two...
if(other.getActiveExportPower() > 0)
activeExportCounter += (((double) ms2/2) * other.getActiveExportPower()) / 3600000000;
if(other.getReactiveImportPower() > 0)
reactiveImportCounter += (((double) ms2/2) * other.getReactiveImportPower()) / 3600000000;
if(other.getReactiveExportPower() > 0)
reactiveExportCounter += (((double) ms2/2) * other.getReactiveExportPower()) / 3600000000;
counterEstimated = true;
}
}
}
this->lastUpdateMillis = other.getLastUpdateMillis();
this->packageTimestamp = other.getPackageTimestamp();
this->listType = max(this->listType, other.getListType());
@@ -297,6 +321,7 @@ void AmsData::apply(AmsData& other) {
this->activeExportCounter = other.getActiveExportCounter();
this->reactiveImportCounter = other.getReactiveImportCounter();
this->reactiveExportCounter = other.getReactiveExportCounter();
this->counterEstimated = false;
case 2:
this->listId = other.getListId();
this->meterId = other.getMeterId();
@@ -311,6 +336,7 @@ void AmsData::apply(AmsData& other) {
this->l2voltage = other.getL2Voltage();
this->l3voltage = other.getL3Voltage();
this->threePhase = other.isThreePhase();
this->lastList2UpdateMillis = other.getLastUpdateMillis();
case 1:
this->activeImportPower = other.getActiveImportPower();
}

View File

@@ -51,6 +51,7 @@ public:
private:
unsigned long lastUpdateMillis = 0;
unsigned long lastList2UpdateMillis = 0;
int listType = 0;
unsigned long packageTimestamp = 0;
String listId, meterId, meterType;
@@ -58,7 +59,7 @@ private:
int activeImportPower = 0, reactiveImportPower = 0, activeExportPower = 0, reactiveExportPower = 0;
double l1voltage = 0, l2voltage = 0, l3voltage = 0, l1current = 0, l2current = 0, l3current = 0;
double activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0;
bool threePhase = false;
bool threePhase = false, counterEstimated = false;
void extractFromKaifa(HanReader& hanReader, int listSize);
void extractFromAidon(HanReader& hanReader, int listSize, bool substituteMissing);

View File

@@ -459,11 +459,11 @@ void loop() {
delay(1);
readHanPort();
if(WiFi.status() == WL_CONNECTED) {
ws.loop();
if(eapi.loop()) {
sendPricesToMqtt();
}
//if(eapi.loop()) {
// sendPricesToMqtt();
//}
}
ws.loop();
delay(1); // Needed for auto modem sleep
}
@@ -611,62 +611,77 @@ void mqttMessageReceived(String &topic, String &payload)
}
void sendPricesToMqtt() {
if(strlen(config.getMqttHost()) == 0 || strlen(config.getMqttPublishTopic()) == 0)
return;
if(strcmp(config.getEntsoeApiToken(), "") != 0)
return;
time_t now = time(nullptr);
double min1hr, min3hr, min6hr;
int min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
double min = INT16_MAX, max = INT16_MIN;
double values[48];
for(int i = 0; i < 48; i++) {
double val1 = eapi.getValueForHour(i);
values[i] = val1;
double values[24] = {0};
for(int i = 0; i < 24; i++) {
double val = eapi.getValueForHour(now, i);
values[i] = val;
if(val1 == ENTSOE_NO_VALUE) break;
if(val == ENTSOE_NO_VALUE) break;
if(val1 < min) min = val1;
if(val1 > max) max = val1;
if(val < min) min = val;
if(val > max) max = val;
if(i >= 24) continue; // Only estimate 1hr, 3hr and 6hr cheapest interval for next 24 hrs
if(min1hrIdx == -1 || min1hr > val1) {
min1hr = val1;
if(min1hrIdx == -1 || min1hr > val) {
min1hr = val;
min1hrIdx = i;
}
double val2 = eapi.getValueForHour(i+1);
double val3 = eapi.getValueForHour(i+2);
if(val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue;
double val3hr = val1+val2+val3;
if(min3hrIdx == -1 || min3hr > val3hr) {
min3hr = val3hr;
min3hrIdx = i;
if(i >= 2) {
double val1 = values[i-2];
double val2 = values[i-1];
double val3 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue;
double val3hr = val1+val2+val3;
if(min3hrIdx == -1 || min3hr > val3hr) {
min3hr = val3hr;
min3hrIdx = i-2;
}
}
double val4 = eapi.getValueForHour(i+3);
double val5 = eapi.getValueForHour(i+4);
double val6 = eapi.getValueForHour(i+5);
if(val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue;
double val6hr = val1+val2+val3+val4+val5+val6;
if(min6hrIdx == -1 || min6hr > val6hr) {
min6hr = val6hr;
min6hrIdx = i;
if(i >= 5) {
double val1 = values[i-5];
double val2 = values[i-4];
double val3 = values[i-3];
double val4 = values[i-2];
double val5 = values[i-1];
double val6 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE || val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue;
double val6hr = val1+val2+val3+val4+val5+val6;
if(min6hrIdx == -1 || min6hr > val6hr) {
min6hr = val6hr;
min6hrIdx = i-5;
}
}
}
char ts1hr[21];
if(min1hrIdx != -1) {
tmElements_t tm;
breakTime(time(nullptr) + (SECS_PER_HOUR * min1hrIdx), tm);
breakTime(now + (SECS_PER_HOUR * min1hrIdx), tm);
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[21];
if(min3hrIdx != -1) {
tmElements_t tm;
breakTime(time(nullptr) + (SECS_PER_HOUR * min3hrIdx), tm);
breakTime(now + (SECS_PER_HOUR * min3hrIdx), tm);
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[21];
if(min6hrIdx != -1) {
tmElements_t tm;
breakTime(time(nullptr) + (SECS_PER_HOUR * min6hrIdx), tm);
breakTime(now + (SECS_PER_HOUR * min6hrIdx), tm);
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
@@ -678,33 +693,37 @@ void sendPricesToMqtt() {
json["name"] = config.getMqttClientId();
json["up"] = millis();
JsonObject jp = json.createNestedObject("prices");
for(int i = 0; i < 48; i++) {
double val = values[i];
if(val == ENTSOE_NO_VALUE) break;
jp[String(i)] = serialized(String(val, 4));
}
if(min != INT16_MAX) {
jp["min"] = serialized(String(min, 4));
}
if(max != INT16_MIN) {
jp["max"] = serialized(String(max, 4));
}
if(min1hrIdx != -1) {
jp["cheapest1hr"] = String(ts1hr);
}
if(min3hrIdx != -1) {
jp["cheapest3hr"] = String(ts1hr);
}
if(min6hrIdx != -1) {
jp["cheapest6hr"] = String(ts1hr);
}
for(int i = 0; i < 24; i++) {
double val = values[i];
if(val == ENTSOE_NO_VALUE) break;
jp[String(i)] = serialized(String(val, 4));
}
if(min != INT16_MAX) {
jp["min"] = serialized(String(min, 4));
}
if(max != INT16_MIN) {
jp["max"] = serialized(String(max, 4));
}
if(min1hrIdx != -1) {
jp["cheapest1hr"] = String(ts1hr);
}
if(min3hrIdx != -1) {
jp["cheapest3hr"] = String(ts3hr);
}
if(min6hrIdx != -1) {
jp["cheapest6hr"] = String(ts6hr);
}
String msg;
serializeJson(json, msg);
mqtt.publish(config.getMqttPublishTopic(), msg.c_str());
break;
}
case 1: // RAW
case 2:
// Send updated prices if we have them
if(strcmp(config.getEntsoeApiToken(), "") != 0) {
for(int i = 0; i < 48; i++) {
{
for(int i = 0; i < 24; i++) {
double val = values[i];
if(val == ENTSOE_NO_VALUE) {
mqtt.publish(String(config.getMqttPublishTopic()) + "/price/" + String(i), "");
@@ -712,6 +731,8 @@ void sendPricesToMqtt() {
} else {
mqtt.publish(String(config.getMqttPublishTopic()) + "/price/" + String(i), String(val, 4));
}
mqtt.loop();
delay(10);
}
if(min != INT16_MAX) {
mqtt.publish(String(config.getMqttPublishTopic()) + "/price/min", String(min, 4));
@@ -719,6 +740,7 @@ void sendPricesToMqtt() {
if(max != INT16_MIN) {
mqtt.publish(String(config.getMqttPublishTopic()) + "/price/max", String(max, 4));
}
if(min1hrIdx != -1) {
mqtt.publish(String(config.getMqttPublishTopic()) + "/price/cheapest/1hr", String(ts1hr));
}
@@ -910,7 +932,6 @@ void readHanPort() {
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/import/active/accumulated", String(data.getActiveImportCounter(), 2));
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/export/reactive/accumulated", String(data.getReactiveExportCounter(), 2));
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/export/active/accumulated", String(data.getActiveExportCounter(), 2));
sendPricesToMqtt();
case 2:
// Only send data if changed. ID and Type is sent on the 10s interval only if changed

View File

@@ -40,10 +40,13 @@ void EntsoeApi::setMultiplier(double multiplier) {
char* EntsoeApi::getCurrency() {
return currency;
}
double EntsoeApi::getValueForHour(int hour) {
tmElements_t tm;
time_t cur = time(nullptr);
return getValueForHour(cur, hour);
}
double EntsoeApi::getValueForHour(time_t cur, int hour) {
tmElements_t tm;
if(tz != NULL)
cur = tz->toLocal(cur);
breakTime(cur, tm);

View File

@@ -15,6 +15,7 @@ public:
bool loop();
double getValueForHour(int hour);
double getValueForHour(time_t now, int hour);
char* getCurrency();
void setToken(const char* token);

View File

@@ -295,19 +295,18 @@ void AmsWebServer::indexHtml() {
html.replace("${data.P}", String(data.getActiveImportPower()));
html.replace("${data.PO}", String(data.getActiveExportPower()));
html.replace("${display.export}", config->getProductionCapacity() > 0 ? "" : "none");
html.replace("${text.import}", config->getProductionCapacity() > 0 ? "Import" : "Consumption");
html.replace("${display.nonexport}", config->getProductionCapacity() > 0 ? "none" : "");
html.replace("${text.import}", config->getProductionCapacity() > 0 ? "Import" : "Use");
html.replace("${display.3p}", data.isThreePhase() ? "" : "none");
html.replace("${data.U1}", u1 > 0 ? String(u1, 1) : "");
html.replace("${data.I1}", u1 > 0 ? String(i1, 1) : "");
html.replace("${display.P1}", u1 > 0 ? "" : "none");
html.replace("${data.U2}", u2 > 0 ? String(u2, 1) : "");
html.replace("${data.I2}", u2 > 0 ? String(i2, 1) : "");
html.replace("${display.P2}", u2 > 0 ? "" : "none");
html.replace("${data.U3}", u3 > 0 ? String(u3, 1) : "");
html.replace("${data.I3}", u3 > 0 ? String(i3, 1) : "");
html.replace("${display.P3}", u3 > 0 ? "" : "none");
html.replace("${data.tPI}", tpi > 0 ? String(tpi, 1) : "");
html.replace("${data.tPO}", tpi > 0 ? String(tpo, 1) : "");
@@ -327,6 +326,8 @@ void AmsWebServer::indexHtml() {
html.replace("${wifi.channel}", WiFi.channel() > 0 ? String(WiFi.channel()) : "");
html.replace("${wifi.ssid}", !WiFi.SSID().isEmpty() ? String(WiFi.SSID()) : "");
html.replace("${currentSeconds}", String((uint32_t)(millis64()/1000), 10));
server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN);
server.send_P(200, "text/html", HEAD_HTML);
server.sendContent(html);
@@ -628,6 +629,9 @@ void AmsWebServer::dataJson() {
double tqi = data.getReactiveImportCounter();
double tqo = data.getReactiveExportCounter();
double volt = u1;
double amp = i1;
if(u1 > 0) {
json["data"]["U1"] = u1;
json["data"]["I1"] = i1;
@@ -635,10 +639,13 @@ void AmsWebServer::dataJson() {
if(u2 > 0) {
json["data"]["U2"] = u2;
json["data"]["I2"] = i2;
if(i2 > amp) amp = i2;
}
if(u3 > 0) {
json["data"]["U3"] = u3;
json["data"]["I3"] = i3;
volt = (u1+u2+u3)/3;
if(i3 > amp) amp = i3;
}
if(tpi > 0) {
@@ -650,6 +657,14 @@ void AmsWebServer::dataJson() {
json["p_pct"] = min(data.getActiveImportPower()*100/maxPwr, 100);
json["v"] = volt;
json["v_pct"] = (max((int)volt-207, 1)*100/46);
int maxAmp = config->getMainFuse() == 0 ? 32 : config->getMainFuse();
json["a"] = amp;
json["a_pct"] = amp * 100 / maxAmp;
if(config->getProductionCapacity() > 0) {
int maxPrd = config->getProductionCapacity() * 1000;
json["po_pct"] = min(data.getActiveExportPower()*100/maxPrd, 100);

View File

@@ -1,5 +1,5 @@
var nextVersion;
var im, em;
var im, em, vm, am;
$(function() {
im = $("#importMeter");
if(im && im.gaugeMeter) {
@@ -18,6 +18,24 @@ $(function() {
append: "W"
});
}
vm = $("#voltMeter");
if(vm && vm.gaugeMeter) {
vm.gaugeMeter({
percent: 0,
text: "-",
append: "V"
});
}
am = $("#ampMeter");
if(am && am.gaugeMeter) {
am.gaugeMeter({
percent: 0,
text: "-",
append: "A"
});
}
var meters = $('.SimpleMeter');
@@ -217,10 +235,12 @@ var fetch = function() {
timeout: 10000,
dataType: 'json',
}).done(function(json) {
if(im && em) {
if(im) {
$(".SimpleMeter").hide();
im.show();
em.show();
vm.show();
am.show();
}
for(var id in json) {
@@ -237,8 +257,8 @@ var fetch = function() {
}
if(window.moment) {
$('#currentMillis').html(moment.duration(parseInt(json.uptime_seconds), 'seconds').humanize());
$('#currentMillis').closest('.row').show();
$('.currentSeconds').html(moment.duration(parseInt(json.uptime_seconds), 'seconds').humanize());
$('.currentSeconds').closest('.row').show();
}
if(json.status) {
@@ -304,16 +324,46 @@ var fetch = function() {
});
}
var v = parseFloat(json.v);
if(v > 0) {
var v_pct = parseInt(json.v_pct);
if(vm && vm.gaugeMeter) {
vm.gaugeMeter({
percent: v_pct,
text: v.toFixed(1)
});
}
}
var a = parseFloat(json.a);
if(a > 0) {
var a_pct = parseInt(json.a_pct);
if(am && am.gaugeMeter) {
am.gaugeMeter({
percent: a_pct,
text: a.toFixed(1)
});
}
}
for(var id in json.data) {
var str = json.data[id];
if(isNaN(str)) {
$('#'+id).html(str);
$('.'+id).html(str);
} else {
var num = parseFloat(str);
$('#'+id).html(num.toFixed(1));
$('.'+id).html(num.toFixed(1));
$('#'+id+'-row').show();
$('.'+id+'-row').show();
}
}
var temp = parseInt(json.temp);
if(temp == -127) {
$('.temp').html("N/A");
}
} else {
if(im && im.gaugeMeter) {
im.gaugeMeter({
@@ -330,26 +380,25 @@ var fetch = function() {
append: "W"
});
}
if(vm && vm.gaugeMeter) {
vm.gaugeMeter({
percent: 0,
text: "-"
});
}
if(am && am.gaugeMeter) {
am.gaugeMeter({
percent: 0,
text: "-"
});
}
}
setTimeout(fetch, interval);
}).fail(function() {
setTimeout(fetch, interval*4);
if(im && im.gaugeMeter) {
im.gaugeMeter({
percent: 0,
text: "-",
append: "W"
});
}
if(em && em.gaugeMeter) {
em.gaugeMeter({
percent: 0,
text: "-",
append: "W"
});
}
setStatus("mqtt", "secondary");
setStatus("wifi", "secondary");
setStatus("han", "secondary");

View File

@@ -55,6 +55,18 @@
"LightGreen-DarkGreen" === option.theme && (e > 0 && (t = "#3afc00"), e > 10 && (t = "#39f900"), e > 20 && (t = "#38f600"), e > 30 && (t = "#38f100"), e > 40 && (t = "#37ec00"), e > 50 && (t = "#36e700"), e > 60 && (t = "#34e200"), e > 70 && (t = "#34df00"), e > 80 && (t = "#33db00"), e > 90 && (t = "#32d900")),
"DarkGold-LightGold" === option.theme && (e > 0 && (t = "#ffb800"), e > 10 && (t = "#ffba00"), e > 20 && (t = "#ffbd00"), e > 30 && (t = "#ffc200"), e > 40 && (t = "#ffc600"), e > 50 && (t = "#ffcb00"), e > 60 && (t = "#ffcf00"), e > 70 && (t = "#ffd400"), e > 80 && (t = "#ffd600"), e > 90 && (t = "#ffd900")),
"LightGold-DarkGold" === option.theme && (e > 0 && (t = "#ffd900"), e > 10 && (t = "#ffd600"), e > 20 && (t = "#ffd400"), e > 30 && (t = "#ffcf00"), e > 40 && (t = "#ffcb00"), e > 50 && (t = "#ffc600"), e > 60 && (t = "#ffc200"), e > 70 && (t = "#ffbd00"), e > 80 && (t = "#ffba00"), e > 90 && (t = "#ffb800")),
"Voltage" === option.theme && (
e > 0 && (t = "#d90000"),
e > 10 && (t = "#f35100"),
e > 20 && (t = "#ffb800"),
e > 30 && (t = "#a6d900"),
e > 40 && (t = "#32d900"),
e > 50 && (t = "#32d900"),
e > 60 && (t = "#a6d900"),
e > 70 && (t = "#ffb800"),
e > 80 && (t = "#f35100"),
e > 90 && (t = "#d90000")
),
"White" === option.theme && (t = "#fff"),
"Black" === option.theme && (t = "#000"),
t;

View File

@@ -49,7 +49,7 @@
</head>
<body class="bg-light">
<main role="main" class="container">
<header class="navbar navbar-expand navbar-dark flex-column flex-lg-row rounded mt-2 mb-4" style="background-color: var(--purple);">
<header class="navbar navbar-expand navbar-dark flex-column flex-lg-row rounded mt-2 mb-3" style="background-color: var(--purple);">
<a href="/" class=""><h6 class="navbar-brand">AMS reader <small id="swVersion" data-url="https://api.github.com/repos/gskjold/AmsToMqttBridge/releases">${version}</small></h6></a>
<div class="navbar-nav-scroll">
<ul class="navbar-nav bd-navbar-nav flex-row">

View File

@@ -1,107 +1,138 @@
<div class="my-3 p-3 bg-white rounded shadow">
<div class="bg-white rounded shadow p-1">
<div class="row">
<div class="col-sm-6 col-lg-3">
<div class="text-center">
<div id="P" class="SimpleMeter" style="display: inline;">
${data.P} W
<div class="col-md-3 col-6">
<div class="text-center">Up <span class="currentSeconds">${currentSeconds}</span></div>
</div>
<div class="col-md-3 col-6">
<div class="text-center">Temperature: <span class="temp">${temp}</span>&deg;C</div>
</div>
<div class="col-md-3 col-6">
<div class="text-center">ESP volt: <span class="vcc">${vcc}</span>V</div>
</div>
<div class="col-md-3 col-6">
<div class="text-center">WiFi RSSI: <span class="rssi">${wifi.rssi}</span>dBm</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="row">
<div class="col-sm-6 mt-3">
<div class="bg-white rounded shadow p-3">
<div class="text-center">
<div id="P" class="SimpleMeter" style="display: inline;">
${data.P} W
</div>
<div id="importMeter" class="GaugeMeter rounded"
style="display: none;"
data-size="180px"
data-text_size="0.15"
data-width="25"
data-style="Arch"
data-theme="Green-Gold-Red"
data-animationstep="0"
data-label="${text.import}"
></div>
</div>
<div id="tPI-row" class="row" style="display: ${display.accumulative};">
<div class="col-12 text-right"><span class="tPI">${data.tPI}</span> kWh</div>
</div>
</div>
<div id="importMeter" class="GaugeMeter rounded"
</div>
<div class="col-sm-6 mt-3" style="display: ${display.export};">
<div class="bg-white rounded shadow p-3">
<div class="text-center">
<div id="P" class="SimpleMeter" style="display: inline;">
${data.PO} W
</div>
<div id="exportMeter" class="GaugeMeter rounded"
style="display: none;"
data-size="180px"
data-text_size="0.15"
data-width="25"
data-style="Arch"
data-theme="DarkGreen-LightGreen"
data-animationstep="0"
data-label="Export"
></div>
</div>
<div id="tPO-row" class="row" style="display: ${display.accumulative};">
<div class="col-12 text-right"><span class="tPO">${data.tPO}</span> kWh</div>
</div>
</div>
</div>
<div class="col-sm-6 mt-3" style="display: ${display.nonexport};">
<div class="bg-white rounded shadow p-3" style="display: ${display.accumulative};">
<h5 class="text-center">Reactive</h5>
<div id="tQI-row" class="row">
<div class="col-4">Import</div>
<div class="col-8 text-right"><span class="tQI">${data.tQI}</span> kvarh</div>
<div class="col-4">Export</div>
<div class="col-8 text-right"><span class="tQO">${data.tQO}</span> kvarh</div>
</div>
</div>
</div>
<div class="col-sm-12 mt-3" style="display: ${display.export};">
<div class="bg-white rounded shadow p-3" style="display: ${display.accumulative};">
<div id="tQO-row" class="row">
<div class="col-4">Import</div>
<div class="col-8 text-right"><span class="tQI">${data.tQI}</span> kvarh</div>
<div class="col-4">Export</div>
<div class="col-8 text-right"><span class="tQO">${data.tQO}</span> kvarh</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 mt-3">
<div class="bg-white rounded shadow p-3">
<div class="text-center">
<div id="voltMeter" class="GaugeMeter rounded"
style="display: none;"
data-size="200px"
data-text_size="0.11"
data-size="180px"
data-text_size="0.15"
data-width="25"
data-style="Arch"
data-theme="Voltage"
data-animationstep="0"
data-label="Volt"
></div>
</div>
<div id="U2-row" class="row" style="display: ${display.3p};">
<div class="col-4" title="L1"><span class="U1">${data.U1}</span>V</div>
<div class="col-4 text-center" title="L2"><span class="U2">${data.U2}</span>V</div>
<div class="col-4 text-right" title="L3"><span class="U3">${data.U3}</span>V</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 mb-3 mt-3">
<div class="bg-white rounded shadow p-3">
<div class="text-center">
<div id="ampMeter" class="GaugeMeter rounded"
style="display: none;"
data-size="180px"
data-text_size="0.15"
data-width="25"
data-style="Arch"
data-theme="Green-Gold-Red"
data-animationstep="0"
data-label="${text.import}"
data-label="Ampere"
></div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div id="U1-row" class="row" style="display: ${display.P1};">
<div class="col-2">L1</div>
<div class="col-5 text-right"><span id="U1">${data.U1}</span> V</div>
<div class="col-5 text-right"><span id="I1">${data.I1}</span> A</div>
</div>
<div id="U2-row" class="row" style="display: ${display.P2};">
<div class="col-2">L2</div>
<div class="col-5 text-right"><span id="U2">${data.U2}</span> V</div>
<div class="col-5 text-right"><span id="I2">${data.I2}</span> A</div>
</div>
<div id="U3-row" class="row" style="display: ${display.P3};">
<div class="col-2">L3</div>
<div class="col-5 text-right"><span id="U3">${data.U3}</span> V</div>
<div class="col-5 text-right"><span id="I3">${data.I3}</span> A</div>
</div>
<hr/>
<div id="tPI-row" class="row" style="display: ${display.accumulative};">
<div class="col-6">Active in</div>
<div class="col-6 text-right"><span id="tPI">${data.tPI}</span> kWh</div>
</div>
<div id="tPO-row" class="row" style="display: ${display.accumulative};">
<div class="col-6">Active out</div>
<div class="col-6 text-right"><span id="tPO">${data.tPO}</span> kWh</div>
</div>
<div id="tQI-row" class="row" style="display: ${display.accumulative};">
<div class="col-6">Reactive in</div>
<div class="col-6 text-right"><span id="tQI">${data.tQI}</span> kvarh</div>
</div>
<div id="tQO-row" class="row" style="display: ${display.accumulative};">
<div class="col-6">Reactive out</div>
<div class="col-6 text-right"><span id="tQO">${data.tQO}</span> kvarh</div>
<div id="I2-row" class="row" style="display: ${display.3p};">
<div class="col-4" title="L1"><span class="I1">${data.I1}</span>A</div>
<div class="col-4 text-center" title="L2"><span class="I2">${data.I2}</span>A</div>
<div class="col-4 text-right" title="L3"><span class="I3">${data.I3}</span>A</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="text-center" style="display: ${display.export};">
<div id="P" class="SimpleMeter" style="display: inline;">
${data.PO} W
</div>
<div id="exportMeter" class="GaugeMeter rounded"
style="display: none;"
data-size="200px"
data-text_size="0.11"
data-width="25"
data-style="Arch"
data-theme="DarkGreen-LightGreen"
data-animationstep="0"
data-label="Export"
></div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<hr class="d-md-inline"/>
<div class="row">
<div class="col-6">Vcc</div>
<div class="col-6 text-right"><span id="vcc">${vcc}</span> V</div>
</div>
<div class="row" style="display: ${display.temp};">
<div class="col-6">Temperature</div>
<div class="col-6 text-right"><span id="temp">${temp}</span> &deg;C</div>
</div>
<div class="row" style="display: none;">
<div class="col-6">Uptime</div>
<div class="col-6 text-right"><span id="currentMillis">${currentMillis}</span></div>
</div>
<hr/>
<div class="row">
<div class="col-6">SSID</div>
<div class="col-6 text-right"><span id="ssid">${wifi.ssid}</span></div>
</div>
<div class="row">
<div class="col-6">Channel</div>
<div class="col-6 text-right"><span id="channel">${wifi.channel}</span></div>
</div>
<div class="row">
<div class="col-6">RSSI</div>
<div class="col-6 text-right"><span id="rssi">${wifi.rssi}</span> dBm</div>
</div>
<hr class="d-none mqtt-error mqtt-error-1 mqtt-error-2 mqtt-error-3 mqtt-error-4 mqtt-error-5 mqtt-error-6 mqtt-error-7 mqtt-error-8 mqtt-error-9 mqtt-error-10 mqtt-error-11 mqtt-error-12 mqtt-error-13"/>
<div class="d-none badge badge-danger mqtt-error mqtt-error-1 mqtt-error-2 mqtt-error-5 mqtt-error-6 mqtt-error-7 mqtt-error-8 mqtt-error-9 mqtt-error-12">MQTT communication error (<span id="mqtt-lastError">-</span>)</div>
<div class="d-none badge badge-danger mqtt-error mqtt-error-3">MQTT failed to connect</div>
<div class="d-none badge badge-danger mqtt-error mqtt-error-4">MQTT network timeout</div>
<div class="d-none badge badge-danger mqtt-error mqtt-error-10">MQTT connection denied</div>
<div class="d-none badge badge-danger mqtt-error mqtt-error-11">MQTT failed to subscribe</div>
<div class="d-none badge badge-danger mqtt-error mqtt-error-13">MQTT lost connection</div>
</div>
<div class="col-lg-3 col-sm-6 mb-3 d-none mqtt-error mqtt-error-1 mqtt-error-2 mqtt-error-3 mqtt-error-4 mqtt-error-5 mqtt-error-6 mqtt-error-7 mqtt-error-8 mqtt-error-9 mqtt-error-10 mqtt-error-11 mqtt-error-12 mqtt-error-13">
<div class="d-none badge badge-danger mqtt-error mqtt-error-1 mqtt-error-2 mqtt-error-5 mqtt-error-6 mqtt-error-7 mqtt-error-8 mqtt-error-9 mqtt-error-12">MQTT communication error (<span id="mqtt-lastError">-</span>)</div>
<div class="d-none badge badge-danger mqtt-error mqtt-error-3">MQTT failed to connect</div>
<div class="d-none badge badge-danger mqtt-error mqtt-error-4">MQTT network timeout</div>
<div class="d-none badge badge-danger mqtt-error mqtt-error-10">MQTT connection denied</div>
<div class="d-none badge badge-danger mqtt-error mqtt-error-11">MQTT failed to subscribe</div>
<div class="d-none badge badge-danger mqtt-error mqtt-error-13">MQTT lost connection</div>
</div>
</div>

View File

@@ -1,4 +1,5 @@
<div id="sensors" class="my-3 p-3 bg-white rounded shadow">
<p>Price retrieval requires ENTSO-E API and NTP to be configured and working</p>
<div class="row">
<div class="col-lg-2 col-md-3 col-sm-4">
<div class="row">