Merge remote-tracking branch 'tbnobody/OpenDTU/master'

This commit is contained in:
helgeerbe 2022-11-03 09:27:56 +01:00
commit b3295f5f33
45 changed files with 331 additions and 194 deletions

View File

@ -24,6 +24,9 @@ serial will be replaced with the serial number of the inverter.
| [serial]/device/fwbuilddatetime | R | Build date / time of inverter firmware | |
| [serial]/device/hwpartnumber | R | Hardware part number of the inverter | |
| [serial]/device/hwversion | R | Hardware version of the inverter | |
| [serial]/status/reachable | R | Indicates whether the inverter is reachable | 0 or 1 |
| [serial]/status/producing | R | Indicates whether the inverter is producing AC power | 0 or 1 |
| [serial]/status/last_update | R | Unix timestamp of last inverter statistics udpate | seconds since JAN 01 1970 (UTC) |
### AC channel / global specific topics
@ -62,8 +65,6 @@ cmd topics are used to set values. Status topics are updated from values set in
| ----------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
| [serial]/status/limit_relative | R | Current applied production limit of the inverter | % of total possible output |
| [serial]/status/limit_absolute | R | Current applied production limit of the inverter | Watt (W) |
| [serial]/status/reachable | R | Indicates whether the inverter is reachable | 0 or 1 |
| [serial]/status/producing | R | Indicates whether the inverter is producing AC power | 0 or 1 |
| [serial]/cmd/limit_persistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will survive the night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % |
| [serial]/cmd/limit_persistent_absolute | W | Set the inverter limit as a absolute value. The value will survive the night without power. The updated value will show up in the web GUI and limit_relative topic after around 4 minutes. | Watt (W) |
| [serial]/cmd/limit_nonpersistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % |

View File

@ -44,7 +44,7 @@ const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = {
{ FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT },
{ FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT },
{ FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT },
{ FLD_PCT, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT },
{ FLD_PF, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT },
{ FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE },
{ FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE },
{ FLD_PRA, DEVICE_CLS_REACTIVE_POWER, STATE_CLS_MEASUREMENT }

View File

@ -30,7 +30,7 @@ private:
FLD_PAC,
FLD_F,
FLD_T,
FLD_PCT,
FLD_PF,
FLD_EFF,
FLD_IRR,
FLD_PRA

View File

@ -13,7 +13,8 @@ public:
private:
void generateJsonResponse(JsonVariant& root);
void addField(JsonVariant& root, uint8_t idx, std::shared_ptr<InverterAbstract> inv, uint8_t channel, uint8_t fieldId, String topic = "");
void addField(JsonObject& root, uint8_t idx, std::shared_ptr<InverterAbstract> inv, uint8_t channel, uint8_t fieldId, String topic = "");
void addTotalField(JsonObject& root, String name, float value, String unit, uint8_t digits);
void onLivedataStatus(AsyncWebServerRequest* request);
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);

View File

@ -3,6 +3,7 @@
* CommandAbstract
* DevControlCommand
* ActivePowerControlCommand
* PowerControlCommand
* MultiDataCommand
* AlarmDataCommand
* DevInfoAllCommand

View File

@ -24,9 +24,9 @@ private:
{ FLD_PAC, UNIT_W, CH0, 18, 2, 10, false },
{ FLD_PRA, UNIT_VA, CH0, 20, 2, 10, false },
{ FLD_F, UNIT_HZ, CH0, 16, 2, 100, false },
{ FLD_PCT, UNIT_PCT, CH0, 24, 2, 10, false },
{ FLD_PF, UNIT_NONE, CH0, 24, 2, 1000, false },
{ FLD_T, UNIT_C, CH0, 26, 2, 10, true },
{ FLD_EVT_LOG, UNIT_CNT, CH0, 28, 2, 1, false },
{ FLD_EVT_LOG, UNIT_NONE, CH0, 28, 2, 1, false },
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC, false },
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC, false },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC, false },

View File

@ -31,9 +31,9 @@ private:
{ FLD_PAC, UNIT_W, CH0, 30, 2, 10, false },
{ FLD_PRA, UNIT_VA, CH0, 32, 2, 10, false },
{ FLD_F, UNIT_HZ, CH0, 28, 2, 100, false },
{ FLD_PCT, UNIT_PCT, CH0, 36, 2, 10, false },
{ FLD_PF, UNIT_NONE, CH0, 36, 2, 1000, false },
{ FLD_T, UNIT_C, CH0, 38, 2, 10, true },
{ FLD_EVT_LOG, UNIT_CNT, CH0, 40, 2, 1, false },
{ FLD_EVT_LOG, UNIT_NONE, CH0, 40, 2, 1, false },
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC, false },
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC, false },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC, false },

View File

@ -45,9 +45,9 @@ private:
{ FLD_PAC, UNIT_W, CH0, 50, 2, 10, false },
{ FLD_PRA, UNIT_VA, CH0, 52, 2, 10, false },
{ FLD_F, UNIT_HZ, CH0, 48, 2, 100, false },
{ FLD_PCT, UNIT_PCT, CH0, 56, 2, 10, false },
{ FLD_PF, UNIT_NONE, CH0, 56, 2, 1000, false },
{ FLD_T, UNIT_C, CH0, 58, 2, 10, true },
{ FLD_EVT_LOG, UNIT_CNT, CH0, 60, 2, 1, false },
{ FLD_EVT_LOG, UNIT_NONE, CH0, 60, 2, 1, false },
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC, false },
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC, false },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC, false },

View File

@ -120,6 +120,25 @@ const char* StatisticsParser::getChannelFieldName(uint8_t channel, uint8_t field
return fields[b[pos].fieldId];
}
uint8_t StatisticsParser::getChannelFieldDigits(uint8_t channel, uint8_t fieldId)
{
uint8_t pos = getAssignIdxByChannelField(channel, fieldId);
const byteAssign_t* b = _byteAssignment;
switch (b[pos].div) {
case 1:
return 0;
case 10:
return 1;
case 100:
return 2;
case 1000:
return 3;
default:
return 2;
}
}
uint8_t StatisticsParser::getChannelCount()
{
const byteAssign_t* b = _byteAssignment;

View File

@ -16,7 +16,7 @@ enum {
UNIT_C,
UNIT_PCT,
UNIT_VA,
UNIT_CNT
UNIT_NONE
};
const char* const units[] = { "V", "A", "W", "Wh", "kWh", "Hz", "°C", "%", "var", "" };
@ -32,7 +32,7 @@ enum {
FLD_PAC,
FLD_F,
FLD_T,
FLD_PCT,
FLD_PF,
FLD_EFF,
FLD_IRR,
FLD_PRA,
@ -83,6 +83,7 @@ public:
bool hasChannelFieldValue(uint8_t channel, uint8_t fieldId);
const char* getChannelFieldUnit(uint8_t channel, uint8_t fieldId);
const char* getChannelFieldName(uint8_t channel, uint8_t fieldId);
uint8_t getChannelFieldDigits(uint8_t channel, uint8_t fieldId);
uint8_t getChannelCount();

View File

@ -71,6 +71,12 @@ void MqttPublishingClass::loop()
MqttSettings.publish(subtopic + "/status/reachable", String(inv->isReachable()));
MqttSettings.publish(subtopic + "/status/producing", String(inv->isProducing()));
if (inv->Statistics()->getLastUpdate() > 0) {
MqttSettings.publish(subtopic + "/status/last_update", String(std::time(0) - (millis() - inv->Statistics()->getLastUpdate()) / 1000));
} else {
MqttSettings.publish(subtopic + "/status/last_update", String(0));
}
uint32_t lastUpdate = inv->Statistics()->getLastUpdate();
if (lastUpdate > 0 && lastUpdate != _lastPublishStats[i]) {
_lastPublishStats[i] = lastUpdate;

View File

@ -73,57 +73,78 @@ void WebApiWsLiveClass::loop()
void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
{
JsonArray invArray = root.createNestedArray("inverters");
float totalPower = 0;
float totalYieldDay = 0;
float totalYieldTotal = 0;
// Loop all inverters
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i);
if (inv == nullptr) {
continue;
}
root[i][F("serial")] = inv->serialString();
root[i][F("name")] = inv->name();
root[i][F("data_age")] = (millis() - inv->Statistics()->getLastUpdate()) / 1000;
root[i][F("reachable")] = inv->isReachable();
root[i][F("producing")] = inv->isProducing();
root[i][F("limit_relative")] = inv->SystemConfigPara()->getLimitPercent();
JsonObject invObject = invArray.createNestedObject();
invObject[F("serial")] = inv->serialString();
invObject[F("name")] = inv->name();
invObject[F("data_age")] = (millis() - inv->Statistics()->getLastUpdate()) / 1000;
invObject[F("reachable")] = inv->isReachable();
invObject[F("producing")] = inv->isProducing();
invObject[F("limit_relative")] = inv->SystemConfigPara()->getLimitPercent();
if (inv->DevInfo()->getMaxPower() > 0) {
root[i][F("limit_absolute")] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0;
invObject[F("limit_absolute")] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0;
} else {
root[i][F("limit_absolute")] = -1;
invObject[F("limit_absolute")] = -1;
}
// Loop all channels
for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) {
addField(root, i, inv, c, FLD_PAC);
addField(root, i, inv, c, FLD_UAC);
addField(root, i, inv, c, FLD_IAC);
addField(invObject, i, inv, c, FLD_PAC);
addField(invObject, i, inv, c, FLD_UAC);
addField(invObject, i, inv, c, FLD_IAC);
if (c == 0) {
addField(root, i, inv, c, FLD_PDC, F("Power DC"));
addField(invObject, i, inv, c, FLD_PDC, F("Power DC"));
} else {
addField(root, i, inv, c, FLD_PDC);
addField(invObject, i, inv, c, FLD_PDC);
}
addField(root, i, inv, c, FLD_UDC);
addField(root, i, inv, c, FLD_IDC);
addField(root, i, inv, c, FLD_YD);
addField(root, i, inv, c, FLD_YT);
addField(root, i, inv, c, FLD_F);
addField(root, i, inv, c, FLD_T);
addField(root, i, inv, c, FLD_PCT);
addField(root, i, inv, c, FLD_PRA);
addField(root, i, inv, c, FLD_EFF);
addField(root, i, inv, c, FLD_IRR);
addField(invObject, i, inv, c, FLD_UDC);
addField(invObject, i, inv, c, FLD_IDC);
addField(invObject, i, inv, c, FLD_YD);
addField(invObject, i, inv, c, FLD_YT);
addField(invObject, i, inv, c, FLD_F);
addField(invObject, i, inv, c, FLD_T);
addField(invObject, i, inv, c, FLD_PF);
addField(invObject, i, inv, c, FLD_PRA);
addField(invObject, i, inv, c, FLD_EFF);
addField(invObject, i, inv, c, FLD_IRR);
}
if (inv->Statistics()->hasChannelFieldValue(CH0, FLD_EVT_LOG)) {
root[i][F("events")] = inv->EventLog()->getEntryCount();
invObject[F("events")] = inv->EventLog()->getEntryCount();
} else {
root[i][F("events")] = -1;
invObject[F("events")] = -1;
}
if (inv->Statistics()->getLastUpdate() > _newestInverterTimestamp) {
_newestInverterTimestamp = inv->Statistics()->getLastUpdate();
}
}
totalPower += inv->Statistics()->getChannelFieldValue(CH0, FLD_PAC);
totalYieldDay += inv->Statistics()->getChannelFieldValue(CH0, FLD_YD);
totalYieldTotal += inv->Statistics()->getChannelFieldValue(CH0, FLD_YT);
}
void WebApiWsLiveClass::addField(JsonVariant& root, uint8_t idx, std::shared_ptr<InverterAbstract> inv, uint8_t channel, uint8_t fieldId, String topic)
JsonObject totalObj = root.createNestedObject("total");
// todo: Fixed hard coded name, unit and digits
addTotalField(totalObj, "Power", totalPower, "W", 1);
addTotalField(totalObj, "YieldDay", totalYieldDay, "Wh", 0);
addTotalField(totalObj, "YieldTotal", totalYieldTotal, "kWh", 2);
}
void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr<InverterAbstract> inv, uint8_t channel, uint8_t fieldId, String topic)
{
if (inv->Statistics()->hasChannelFieldValue(channel, fieldId)) {
String chanName;
@ -132,11 +153,19 @@ void WebApiWsLiveClass::addField(JsonVariant& root, uint8_t idx, std::shared_ptr
} else {
chanName = topic;
}
root[idx][String(channel)][chanName]["v"] = inv->Statistics()->getChannelFieldValue(channel, fieldId);
root[idx][String(channel)][chanName]["u"] = inv->Statistics()->getChannelFieldUnit(channel, fieldId);
root[String(channel)][chanName]["v"] = inv->Statistics()->getChannelFieldValue(channel, fieldId);
root[String(channel)][chanName]["u"] = inv->Statistics()->getChannelFieldUnit(channel, fieldId);
root[String(channel)][chanName]["d"] = inv->Statistics()->getChannelFieldDigits(channel, fieldId);
}
}
void WebApiWsLiveClass::addTotalField(JsonObject& root, String name, float value, String unit, uint8_t digits)
{
root[name]["v"] = value;
root[name]["u"] = unit;
root[name]["d"] = digits;
}
void WebApiWsLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
{
if (type == WS_EVT_CONNECT) {
@ -152,8 +181,8 @@ void WebApiWsLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketC
void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
{
AsyncJsonResponse* response = new AsyncJsonResponse(true, 40960U);
JsonVariant root = response->getRoot().as<JsonVariant>();
AsyncJsonResponse* response = new AsyncJsonResponse(false, 40960U);
JsonVariant root = response->getRoot();
generateJsonResponse(root);

View File

@ -21,18 +21,18 @@
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@types/bootstrap": "^5.2.5",
"@types/node": "^18.11.4",
"@types/node": "^18.11.8",
"@types/spark-md5": "^3.0.2",
"@vitejs/plugin-vue": "^3.1.2",
"@vitejs/plugin-vue": "^3.2.0",
"@vue/eslint-config-typescript": "^11.0.2",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.26.0",
"eslint-plugin-vue": "^9.6.0",
"eslint-plugin-vue": "^9.7.0",
"npm-run-all": "^4.1.5",
"typescript": "^4.8.4",
"vite": "^3.1.8",
"vite": "^3.2.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^2.1.0",
"vite-plugin-css-injected-by-js": "^2.1.1",
"vue-tsc": "^1.0.9"
}
}

View File

@ -1,6 +1,6 @@
<template>
<div class="card">
<div class="card-header text-white bg-primary">
<div class="card-header text-bg-primary">
Firmware Information
</div>
<div class="card-body">

View File

@ -1,6 +1,6 @@
<template>
<div class="card">
<div class="card-header text-white bg-primary">
<div class="card-header text-bg-primary">
Hardware Information
</div>
<div class="card-body">

View File

@ -1,6 +1,6 @@
<template>
<div class="card">
<div class="card-header text-white bg-primary">
<div class="card-header text-bg-primary">
Network Interface (Access Point)
</div>
<div class="card-body">

View File

@ -1,6 +1,6 @@
<template>
<div class="card">
<div class="card-header text-white bg-primary">
<div class="card-header text-bg-primary">
Network Interface ({{ networkStatus.network_mode }})
</div>
<div class="card-body">

View File

@ -1,7 +1,7 @@
<template>
<div class="card" :class="{ 'border-info': channelNumber == 0 }">
<div v-if="channelNumber >= 1" class="card-header">String {{ channelNumber }}</div>
<div v-if="channelNumber == 0" class="card-header bg-info">Phase {{ channelNumber + 1 }}</div>
<div v-if="channelNumber == 0" class="card-header text-bg-info">Phase {{ channelNumber + 1 }}</div>
<div class="card-body">
<table class="table table-striped table-hover">
<thead>
@ -15,7 +15,7 @@
<tr v-for="(property, key) in channelData" :key="`prop-${key}`">
<template v-if="property">
<th scope="row">{{ key }}</th>
<td style="text-align: right">{{ formatNumber(property.v) }}</td>
<td style="text-align: right">{{ formatNumber(property.v, property.d) }}</td>
<td>{{ property.u }}</td>
</template>
</tr>
@ -28,6 +28,7 @@
<script lang="ts">
import { defineComponent, type PropType } from 'vue';
import type { InverterStatistics } from '@/types/LiveDataStatus';
import { formatNumber } from '@/utils';
export default defineComponent({
props: {
@ -35,11 +36,7 @@ export default defineComponent({
channelNumber: { type: Number, required: true },
},
methods: {
formatNumber(num: number) {
return new Intl.NumberFormat(
undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }
).format(num);
},
formatNumber,
},
});
</script>

View File

@ -0,0 +1,55 @@
<template>
<div class="row row-cols-1 row-cols-md-3 g-3">
<div class="col">
<div class="card">
<div class="card-header text-bg-success">Total Yield Total</div>
<div class="card-body">
<p class="card-text text-center">
<h2>{{ formatNumber(totalData.YieldTotal.v, totalData.YieldTotal.d) }}
<small class="text-muted">{{ totalData.YieldTotal.u }}</small>
</h2>
</p>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header text-bg-success">Total Yield Day</div>
<div class="card-body">
<p class="card-text text-center">
<h2>{{ formatNumber(totalData.YieldDay.v, totalData.YieldDay.d) }}
<small class="text-muted">{{ totalData.YieldDay.u }}</small>
</h2>
</p>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header text-bg-success">Total Power</div>
<div class="card-body">
<p class="card-text text-center">
<h2>{{ formatNumber(totalData.Power.v, totalData.Power.d) }}
<small class="text-muted">{{ totalData.Power.u }}</small>
</h2>
</p>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, type PropType } from 'vue';
import type { Total } from '@/types/LiveDataStatus';
import { formatNumber } from '@/utils';
export default defineComponent({
props: {
totalData: { type: Object as PropType<Total>, required: true },
},
methods: {
formatNumber,
},
});
</script>

View File

@ -3,7 +3,7 @@
<tbody>
<tr>
<td>Current Limit</td>
<td>{{ formatNumber(limitData.limit) }}%</td>
<td>{{ formatNumber(limitData.limit, 2) }}%</td>
</tr>
</tbody>
</table>
@ -11,6 +11,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { formatNumber } from '@/utils';
declare interface LimitData {
limit: number,
@ -20,14 +21,8 @@ export default defineComponent({
props: {
limitData: { type: Object as () => LimitData, required: true },
},
computed: {
formatNumber() {
return (num: number) => {
return new Intl.NumberFormat(
undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }
).format(num)
};
},
},
methods: {
formatNumber,
}
});
</script>

View File

@ -1,6 +1,6 @@
<template>
<div class="card">
<div class="card-header text-white bg-primary">Memory Information</div>
<div class="card-header text-bg-primary">Memory Information</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-condensed">

View File

@ -1,7 +1,7 @@
<template>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">OpenDTU</a>
<a class="navbar-brand" href="#"><span class="text-warning"><BIconSun width="30" height="30" class="d-inline-block align-text-top"/></span> OpenDTU</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
@ -85,8 +85,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { BIconSun } from 'bootstrap-icons-vue';
export default defineComponent({
components: {
BIconSun,
},
methods: {
onClick() {
this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove("show");

View File

@ -1,6 +1,6 @@
<template>
<div class="card">
<div class="card-header text-white bg-primary">
<div class="card-header text-bg-primary">
Radio Information
</div>
<div class="card-body">
@ -10,8 +10,8 @@
<tr>
<th>Chip Status</th>
<td class="badge" :class="{
'bg-danger': !systemStatus.radio_connected,
'bg-success': systemStatus.radio_connected,
'text-bg-danger': !systemStatus.radio_connected,
'text-bg-success': systemStatus.radio_connected,
}">
<span v-if="systemStatus.radio_connected">connected</span>
<span v-else>not connected</span>
@ -20,9 +20,9 @@
<tr>
<th>Chip Type</th>
<td class="badge" :class="{
'bg-danger': systemStatus.radio_connected && !systemStatus.radio_pvariant,
'bg-success': systemStatus.radio_connected && systemStatus.radio_pvariant,
'bg-secondary': !systemStatus.radio_connected,
'text-bg-danger': systemStatus.radio_connected && !systemStatus.radio_pvariant,
'text-bg-success': systemStatus.radio_connected && systemStatus.radio_pvariant,
'text-bg-secondary': !systemStatus.radio_connected,
}">
<span
v-if="systemStatus.radio_connected && systemStatus.radio_pvariant">nRF24L01+</span>

View File

@ -1,6 +1,6 @@
<template>
<div class="card">
<div class="card-header text-white bg-primary">
<div class="card-header text-bg-primary">
WiFi Information (Access Point)
</div>
<div class="card-body">
@ -10,8 +10,8 @@
<tr>
<th>Status</th>
<td class="badge" :class="{
'bg-danger': !networkStatus.ap_status,
'bg-success': networkStatus.ap_status,
'text-bg-danger': !networkStatus.ap_status,
'text-bg-success': networkStatus.ap_status,
}">
<span v-if="networkStatus.ap_status">enabled</span>
<span v-else>disabled</span>

View File

@ -1,6 +1,6 @@
<template>
<div class="card">
<div class="card-header text-white bg-primary">
<div class="card-header text-bg-primary">
WiFi Information (Station)
</div>
<div class="card-body">
@ -10,8 +10,8 @@
<tr>
<th>Status</th>
<td class="badge" :class="{
'bg-danger': !networkStatus.sta_status,
'bg-success': networkStatus.sta_status,
'text-bg-danger': !networkStatus.sta_status,
'text-bg-success': networkStatus.sta_status,
}">
<span v-if="networkStatus.sta_status">enabled</span>
<span v-else>disabled</span>

View File

@ -1,6 +1,7 @@
export interface ValueObject {
v: number, // value
u: string, // unit
d: number, // digits
};
export interface InverterStatistics {
@ -30,7 +31,16 @@ export interface Inverter {
[key: number]: InverterStatistics,
};
export interface Inverters extends Array<Inverter>{};
export interface Total {
Power: ValueObject,
YieldDay: ValueObject,
YieldTotal: ValueObject,
};
export interface LiveData {
inverters: Inverter[],
total: Total,
}
// Ve.Direct
export interface Vedirect {

View File

@ -1,9 +1,12 @@
import { timestampToString } from './time';
import { formatNumber } from './number';
export {
timestampToString,
formatNumber,
};
export default {
timestampToString,
formatNumber,
}

View File

@ -0,0 +1,5 @@
export const formatNumber = (num: number, digits: number): string => {
return new Intl.NumberFormat(
undefined, { minimumFractionDigits: digits, maximumFractionDigits: digits }
).format(num);
}

View File

@ -5,7 +5,7 @@
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<span class="badge bg-secondary">
<span class="badge text-bg-secondary">
<BIconInfoCircle class="fs-4" />
</span>&nbsp;Project Origin
</button>
@ -41,7 +41,7 @@
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<span class="badge bg-secondary">
<span class="badge text-bg-secondary">
<BIconActivity class="fs-4" />
</span>&nbsp;News &amp; Updates
</button>
@ -58,7 +58,7 @@
<h2 class="accordion-header" id="headingThree">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
<span class="badge bg-secondary">
<span class="badge text-bg-secondary">
<BIconBug class="fs-4" />
</span>&nbsp;Error Reporting
</button>
@ -75,7 +75,7 @@
<h2 class="accordion-header" id="headingFour">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
<span class="badge bg-secondary">
<span class="badge text-bg-secondary">
<BIconChat class="fs-4" />
</span>&nbsp;Discussion
</button>

View File

@ -5,7 +5,7 @@
</BootstrapAlert>
<div class="card">
<div class="card-header text-white bg-primary">Backup: Configuration File Backup</div>
<div class="card-header text-bg-primary">Backup: Configuration File Backup</div>
<div class="card-body text-center">
Backup the configuration file
<button class="btn btn-primary" @click="downloadConfig">Backup
@ -14,7 +14,7 @@
</div>
<div class="card mt-5">
<div class="card-header text-white bg-primary">Restore: Restore the Configuration File</div>
<div class="card-header text-bg-primary">Restore: Restore the Configuration File</div>
<div class="card-body text-center">
<div v-if="!uploading && UploadError != ''">
@ -66,7 +66,7 @@
</div>
<div class="card mt-5">
<div class="card-header text-white bg-primary">Initialize: Perform Factory Reset</div>
<div class="card-header text-bg-primary">Initialize: Perform Factory Reset</div>
<div class="card-body text-center">
<button class="btn btn-danger" @click="onFactoryResetModal">Restore Factory-Default Settings

View File

@ -6,7 +6,7 @@
<form @submit="saveDtuConfig">
<div class="card">
<div class="card-header text-white bg-primary">DTU Configuration</div>
<div class="card-header text-bg-primary">DTU Configuration</div>
<div class="card-body">
<div class="row mb-3">
<label for="inputDtuSerial" class="col-sm-2 col-form-label">Serial:</label>

View File

@ -9,7 +9,7 @@
</div>
<div v-if="!loading && !uploading && OTAError != ''" class="card">
<div class="card-header text-white bg-danger">OTA Error</div>
<div class="card-header text-bg-danger">OTA Error</div>
<div class="card-body text-center">
<p class="h1 mb-2">
<BIconExclamationCircleFill />
@ -30,12 +30,12 @@
</div>
<div v-else-if="!loading && !uploading && OTASuccess" class="card">
<div class="card-header text-white bg-success">OTA Status</div>
<div class="card-header text-bg-success">OTA Status</div>
<div class="card-body text-center">
<span class="h1 mb-2">
<BIconCheckCircle />
</span>
<span> OTA Success </span>
<span> OTA Success. The unit has been automatically restarted and will be available again in a few moments. </span>
<br />
<br />
<button class="btn btn-primary" @click="clear">
@ -45,7 +45,7 @@
</div>
<div v-else-if="!loading && !uploading" class="card">
<div class="card-header text-white bg-primary">Firmware Upload</div>
<div class="card-header text-bg-primary">Firmware Upload</div>
<div class="card-body text-center">
<div class="form-group pt-2 mt-3">
<input class="form-control" type="file" ref="file" accept=".bin,.bin.gz" @change="uploadOTA" />
@ -54,7 +54,7 @@
</div>
<div v-else-if="!loading && uploading" class="card">
<div class="card-header text-white bg-primary">Upload Progress</div>
<div class="card-header text-bg-primary">Upload Progress</div>
<div class="card-body text-center">
<div class="progress">
<div class="progress-bar" role="progressbar" :style="{ width: progress + '%' }"

View File

@ -1,5 +1,6 @@
<template>
<BasePage :title="'Live Data'" :isLoading="dataLoading" :isWideScreen="true">
<InverterTotalInfo :totalData="liveData.total" /><br />
<div class="row gy-3">
<div class="col-sm-3 col-md-2" :style="[inverterData.length == 1 ? { 'display': 'none' } : {}]">
<div class="nav nav-pills row-cols-sm-1" id="v-pills-tab" role="tablist" aria-orientation="vertical">
@ -15,17 +16,19 @@
</div>
</div>
<div class="tab-content" id="v-pills-tabContent" :class="{'col-sm-9 col-md-10': inverterData.length > 1,
'col-sm-12 col-md-12': inverterData.length == 1 }">
<div class="tab-content" id="v-pills-tabContent" :class="{
'col-sm-9 col-md-10': inverterData.length > 1,
'col-sm-12 col-md-12': inverterData.length == 1
}">
<div v-for="inverter in inverterData" :key="inverter.serial" class="tab-pane fade show"
:id="'v-pills-' + inverter.serial" role="tabpanel"
:aria-labelledby="'v-pills-' + inverter.serial + '-tab'" tabindex="0">
<div class="card">
<div class="card-header text-white bg-primary d-flex justify-content-between align-items-center"
<div class="card-header d-flex justify-content-between align-items-center"
:class="{
'bg-danger': !inverter.reachable,
'bg-warning': inverter.reachable && !inverter.producing,
'bg-primary': inverter.reachable && inverter.producing,
'text-bg-danger': !inverter.reachable,
'text-bg-warning': inverter.reachable && !inverter.producing,
'text-bg-primary': inverter.reachable && inverter.producing,
}">
<div class="p-1 flex-grow-1">
<div class="d-flex flex-wrap">
@ -37,9 +40,8 @@
</div>
<div style="padding-right: 2em;">
Current Limit: <template v-if="inverter.limit_absolute > -1"> {{
inverter.limit_absolute.toFixed(0) }}W | </template>{{
inverter.limit_relative.toFixed(0)
}}%
formatNumber(inverter.limit_absolute, 0)
}} W | </template>{{ formatNumber(inverter.limit_relative, 0) }} %
</div>
<div style="padding-right: 2em;">
Data Age: {{ inverter.data_age }} seconds
@ -77,7 +79,7 @@
@click="onShowEventlog(inverter.serial)" title="Show Eventlog">
<BIconJournalText style="font-size:24px;" />
<span
class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-danger">
{{ inverter.events }}
<span class="visually-hidden">unread messages</span>
</span>
@ -180,15 +182,16 @@
Limit:</label>
<div class="col-sm-4">
<div class="input-group">
<input type="number" class="form-control" id="inputCurrentLimit"
aria-describedby="currentLimitType" v-model="currentLimitRelative" disabled />
<input type="text" class="form-control" id="inputCurrentLimit"
aria-describedby="currentLimitType" v-model="currentLimitRelative"
disabled />
<span class="input-group-text" id="currentLimitType">%</span>
</div>
</div>
<div class="col-sm-4" v-if="currentLimitList.max_power > 0">
<div class="input-group">
<input type="number" class="form-control" id="inputCurrentLimitAbsolute"
<input type="text" class="form-control" id="inputCurrentLimitAbsolute"
aria-describedby="currentLimitTypeAbsolute" v-model="currentLimitAbsolute"
disabled />
<span class="input-group-text" id="currentLimitTypeAbsolute">W</span>
@ -201,10 +204,10 @@
Status:</label>
<div class="col-sm-9">
<span class="badge" :class="{
'bg-danger': currentLimitList.limit_set_status == 'Failure',
'bg-warning': currentLimitList.limit_set_status == 'Pending',
'bg-success': currentLimitList.limit_set_status == 'Ok',
'bg-secondary': currentLimitList.limit_set_status == 'Unknown',
'text-bg-danger': currentLimitList.limit_set_status == 'Failure',
'text-bg-warning': currentLimitList.limit_set_status == 'Pending',
'text-bg-success': currentLimitList.limit_set_status == 'Ok',
'text-bg-secondary': currentLimitList.limit_set_status == 'Unknown',
}">
{{ currentLimitList.limit_set_status }}
</span>
@ -276,10 +279,10 @@
Status:</label>
<div class="col">
<span class="badge" :class="{
'bg-danger': successCommandPower == 'Failure',
'bg-warning': successCommandPower == 'Pending',
'bg-success': successCommandPower == 'Ok',
'bg-secondary': successCommandPower == 'Unknown',
'text-bg-danger': successCommandPower == 'Failure',
'text-bg-warning': successCommandPower == 'Pending',
'text-bg-success': successCommandPower == 'Ok',
'text-bg-secondary': successCommandPower == 'Unknown',
}">
{{ successCommandPower }}
</span>
@ -330,17 +333,20 @@ import EventLog from '@/components/EventLog.vue';
import DevInfo from '@/components/DevInfo.vue';
import BootstrapAlert from '@/components/BootstrapAlert.vue';
import InverterChannelInfo from "@/components/InverterChannelInfo.vue";
import InverterTotalInfo from '@/components/InverterTotalInfo.vue';
import VedirectView from '@/views/VedirectView.vue';
import type { DevInfoStatus } from '@/types/DevInfoStatus';
import type { EventlogItems } from '@/types/EventlogStatus';
import type { Inverters } from '@/types/LiveDataStatus';
import type { LiveData, Inverter } from '@/types/LiveDataStatus';
import type { LimitStatus } from '@/types/LimitStatus';
import type { LimitConfig } from '@/types/LimitConfig';
import { formatNumber } from '@/utils';
export default defineComponent({
components: {
BasePage,
InverterChannelInfo,
InverterTotalInfo,
EventLog,
DevInfo,
BootstrapAlert,
@ -362,7 +368,7 @@ export default defineComponent({
heartInterval: 0,
dataAgeInterval: 0,
dataLoading: true,
inverterData: [] as Inverters,
liveData: {} as LiveData,
isFirstFetchAfterConnect: true,
eventLogView: {} as bootstrap.Modal,
eventLogList: {} as EventlogItems,
@ -377,7 +383,7 @@ export default defineComponent({
currentLimitList: {} as LimitStatus,
targetLimitList: {} as LimitConfig,
targetLimitMin: 10,
targetLimitMin: 2,
targetLimitMax: 100,
targetLimitTypeText: "Relative (%)",
targetLimitType: 1,
@ -428,23 +434,27 @@ export default defineComponent({
}
},
computed: {
currentLimitAbsolute(): number {
currentLimitAbsolute(): string {
if (this.currentLimitList.max_power > 0) {
return Number((this.currentLimitList.limit_relative * this.currentLimitList.max_power / 100).toFixed(1));
return formatNumber(this.currentLimitList.limit_relative * this.currentLimitList.max_power / 100, 2);
}
return 0;
return "0";
},
currentLimitRelative(): number {
return Number((this.currentLimitList.limit_relative).toFixed(1));
currentLimitRelative(): string {
return formatNumber(this.currentLimitList.limit_relative, 2);
},
inverterData(): Inverter[] {
return this.liveData.inverters;
}
},
methods: {
formatNumber,
getInitialData() {
this.dataLoading = true;
fetch("/api/livedata/status")
.then((response) => response.json())
.then((data) => {
this.inverterData = data;
this.liveData = data;
this.dataLoading = false;
});
},
@ -459,7 +469,7 @@ export default defineComponent({
this.socket.onmessage = (event) => {
console.log(event);
this.inverterData = JSON.parse(event.data);
this.liveData = JSON.parse(event.data);
this.dataLoading = false;
this.heartCheck(); // Reset heartbeat detection
};
@ -585,7 +595,7 @@ export default defineComponent({
onSelectType(type: number) {
if (type == 1) {
this.targetLimitTypeText = "Relative (%)";
this.targetLimitMin = 10;
this.targetLimitMin = 2;
this.targetLimitMax = 100;
} else {
this.targetLimitTypeText = "Absolute (W)";

View File

@ -5,7 +5,7 @@
</BootstrapAlert>
<div class="card">
<div class="card-header text-white bg-primary">Add a new Inverter</div>
<div class="card-header text-bg-primary">Add a new Inverter</div>
<div class="card-body">
<form class="form-inline" v-on:submit.prevent="onSubmit">
<div class="form-group">
@ -30,7 +30,7 @@
</div>
<div class="card mt-5">
<div class="card-header text-white bg-primary">Inverter List</div>
<div class="card-header text-bg-primary">Inverter List</div>
<div class="card-body">
<div class="table-responsive">
<table class="table">

View File

@ -6,7 +6,7 @@
<form @submit="saveMqttConfig">
<div class="card">
<div class="card-header text-white bg-primary">MqTT Configuration</div>
<div class="card-header text-bg-primary">MqTT Configuration</div>
<div class="card-body">
<div class="row mb-3">
<label class="col-sm-4 form-check-label" for="inputMqtt">Enable MqTT</label>
@ -32,7 +32,7 @@
</div>
<div class="card mt-5" v-show="mqttConfigList.mqtt_enabled">
<div class="card-header text-white bg-primary">
<div class="card-header text-bg-primary">
MqTT Broker Parameter
</div>
<div class="card-body">
@ -127,7 +127,7 @@
</div>
<div class="card mt-5" v-show="mqttConfigList.mqtt_enabled">
<div class="card-header text-white bg-primary">LWT Parameters</div>
<div class="card-header text-bg-primary">LWT Parameters</div>
<div class="card-body">
<div class="row mb-3">
<label for="inputLwtTopic" class="col-sm-2 col-form-label">LWT Topic:</label>
@ -164,7 +164,7 @@
</div>
<div class="card mt-5" v-show="mqttConfigList.mqtt_enabled && mqttConfigList.mqtt_hass_enabled">
<div class="card-header text-white bg-primary">Home Assistant MQTT Auto Discovery Parameters</div>
<div class="card-header text-bg-primary">Home Assistant MQTT Auto Discovery Parameters</div>
<div class="card-body">
<div class="row mb-3">
<label for="inputHassTopic" class="col-sm-2 col-form-label">Prefix Topic:</label>

View File

@ -1,7 +1,7 @@
<template>
<BasePage :title="'MqTT Info'" :isLoading="dataLoading">
<div class="card">
<div class="card-header text-white bg-primary">Configuration Summary</div>
<div class="card-header text-bg-primary">Configuration Summary</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-condensed">
@ -9,8 +9,8 @@
<tr>
<th>Status</th>
<td class="badge" :class="{
'bg-danger': !mqttDataList.mqtt_enabled,
'bg-success': mqttDataList.mqtt_enabled,
'text-bg-danger': !mqttDataList.mqtt_enabled,
'text-bg-success': mqttDataList.mqtt_enabled,
}">
<span v-if="mqttDataList.mqtt_enabled">enabled</span>
<span v-else>disabled</span>
@ -39,8 +39,8 @@
<tr>
<th>Retain</th>
<td class="badge" :class="{
'bg-danger': !mqttDataList.mqtt_retain,
'bg-success': mqttDataList.mqtt_retain,
'text-bg-danger': !mqttDataList.mqtt_retain,
'text-bg-success': mqttDataList.mqtt_retain,
}">
<span v-if="mqttDataList.mqtt_retain">enabled</span>
<span v-else>disabled</span>
@ -49,8 +49,8 @@
<tr>
<th>TLS</th>
<td class="badge" :class="{
'bg-danger': !mqttDataList.mqtt_tls,
'bg-success': mqttDataList.mqtt_tls,
'text-bg-danger': !mqttDataList.mqtt_tls,
'text-bg-success': mqttDataList.mqtt_tls,
}">
<span v-if="mqttDataList.mqtt_tls">enabled</span>
<span v-else>disabled</span>
@ -67,7 +67,7 @@
</div>
<div class="card mt-5">
<div class="card-header text-white bg-primary">Home Assistant MQTT Auto Discovery Configuration Summary
<div class="card-header text-bg-primary">Home Assistant MQTT Auto Discovery Configuration Summary
</div>
<div class="card-body">
<div class="table-responsive">
@ -76,8 +76,8 @@
<tr>
<th>Status</th>
<td class="badge" :class="{
'bg-danger': !mqttDataList.mqtt_hass_enabled,
'bg-success': mqttDataList.mqtt_hass_enabled,
'text-bg-danger': !mqttDataList.mqtt_hass_enabled,
'text-bg-success': mqttDataList.mqtt_hass_enabled,
}">
<span v-if="mqttDataList.mqtt_hass_enabled">enabled</span>
<span v-else>disabled</span>
@ -90,8 +90,8 @@
<tr>
<th>Retain</th>
<td class="badge" :class="{
'bg-danger': !mqttDataList.mqtt_hass_retain,
'bg-success': mqttDataList.mqtt_hass_retain,
'text-bg-danger': !mqttDataList.mqtt_hass_retain,
'text-bg-success': mqttDataList.mqtt_hass_retain,
}">
<span v-if="mqttDataList.mqtt_hass_retain">enabled</span>
<span v-else>disabled</span>
@ -100,8 +100,8 @@
<tr>
<th>Expire</th>
<td class="badge" :class="{
'bg-danger': !mqttDataList.mqtt_hass_expire,
'bg-success': mqttDataList.mqtt_hass_expire,
'text-bg-danger': !mqttDataList.mqtt_hass_expire,
'text-bg-success': mqttDataList.mqtt_hass_expire,
}">
<span v-if="mqttDataList.mqtt_hass_expire">enabled</span>
<span v-else>disabled</span>
@ -110,8 +110,8 @@
<tr>
<th>Individual Panels</th>
<td class="badge" :class="{
'bg-danger': !mqttDataList.mqtt_hass_individualpanels,
'bg-success': mqttDataList.mqtt_hass_individualpanels,
'text-bg-danger': !mqttDataList.mqtt_hass_individualpanels,
'text-bg-success': mqttDataList.mqtt_hass_individualpanels,
}">
<span v-if="mqttDataList.mqtt_hass_individualpanels">enabled</span>
<span v-else>disabled</span>
@ -124,7 +124,7 @@
</div>
<div class="card mt-5">
<div class="card-header text-white bg-primary">Runtime Summary</div>
<div class="card-header text-bg-primary">Runtime Summary</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-condensed">
@ -132,8 +132,8 @@
<tr>
<th>Connection Status</th>
<td class="badge" :class="{
'bg-danger': !mqttDataList.mqtt_connected,
'bg-success': mqttDataList.mqtt_connected,
'text-bg-danger': !mqttDataList.mqtt_connected,
'text-bg-success': mqttDataList.mqtt_connected,
}">
<span v-if="mqttDataList.mqtt_connected">connected</span>
<span v-else>disconnected</span>

View File

@ -6,7 +6,7 @@
<form @submit="saveNetworkConfig">
<div class="card">
<div class="card-header text-white bg-primary">WiFi Configuration</div>
<div class="card-header text-bg-primary">WiFi Configuration</div>
<div class="card-body">
<div class="row mb-3">
<label for="inputSSID" class="col-sm-2 col-form-label">WiFi SSID:</label>
@ -50,7 +50,7 @@
</div>
<div class="card" v-show="!networkConfigList.dhcp">
<div class="card-header text-white bg-primary">
<div class="card-header text-bg-primary">
Static IP Configuration
</div>
<div class="card-body">

View File

@ -6,7 +6,7 @@
<form @submit="saveNtpConfig">
<div class="card">
<div class="card-header text-white bg-primary">NTP Configuration</div>
<div class="card-header text-bg-primary">NTP Configuration</div>
<div class="card-body">
<div class="row mb-3">
<label for="inputNtpServer" class="col-sm-2 col-form-label">Time Server:</label>
@ -41,7 +41,7 @@
</form>
<div class="card">
<div class="card-header text-white bg-primary">Manual Time Synchronization</div>
<div class="card-header text-bg-primary">Manual Time Synchronization</div>
<div class="card-body">
<div class="row mb-3">
<label for="currentMcuTime" class="col-sm-2 col-form-label">Current OpenDTU Time:</label>

View File

@ -1,7 +1,7 @@
<template>
<BasePage :title="'NTP Info'" :isLoading="dataLoading">
<div class="card">
<div class="card-header text-white bg-primary">Configuration Summary</div>
<div class="card-header text-bg-primary">Configuration Summary</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-condensed">
@ -25,7 +25,7 @@
</div>
<div class="card mt-5">
<div class="card-header text-white bg-primary">Current Time</div>
<div class="card-header text-bg-primary">Current Time</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-condensed">
@ -33,8 +33,8 @@
<tr>
<th>Status</th>
<td class="badge" :class="{
'bg-danger': !ntpDataList.ntp_status,
'bg-success': ntpDataList.ntp_status,
'text-bg-danger': !ntpDataList.ntp_status,
'text-bg-success': ntpDataList.ntp_status,
}">
<span v-if="ntpDataList.ntp_status">synced</span>
<span v-else>not synced</span>

View File

@ -6,7 +6,7 @@
<form @submit="savePasswordConfig">
<div class="card">
<div class="card-header text-white bg-primary">Admin password</div>
<div class="card-header text-bg-primary">Admin password</div>
<div class="card-body">
<div class="row mb-3">
<label for="inputPassword" class="col-sm-2 col-form-label">Password:</label>

View File

@ -15,7 +15,7 @@
<template v-if="!dataLoading">
<form @submit="saveVedirectConfig">
<div class="card">
<div class="card-header text-white bg-primary">Ve.direct Configuration</div>
<div class="card-header bg-primary">Ve.direct Configuration</div>
<div class="card-body">
<div class="row mb-3">
<label class="col-sm-2 form-check-label" for="inputVedirect">Enable Ve.direct</label>

View File

@ -12,7 +12,7 @@
<template v-if="!dataLoading">
<div class="card">
<div class="card-header text-white bg-primary">Configuration Summary</div>
<div class="card-header bg-primary">Configuration Summary</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-condensed">

View File

@ -10,7 +10,7 @@
<div class="row gy-3">
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
<div class="card">
<div class="card-header text-white bg-primary d-flex justify-content-between align-items-center"
<div class="card-header d-flex justify-content-between align-items-center"
:class="{
'bg-danger': vedirectData.age_critical,
'bg-primary': !vedirectData.age_critical,

View File

@ -99,10 +99,10 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
"@types/node@^18.11.4":
version "18.11.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.4.tgz#7017a52e18dfaad32f55eebd539993014441949c"
integrity sha512-BxcJpBu8D3kv/GZkx/gSMz6VnTJREBj/4lbzYOQueUOELkt8WrO6zAcSPmp9uRPEW/d+lUO8QK0W2xnS1hEU0A==
"@types/node@^18.11.8":
version "18.11.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.8.tgz#16d222a58d4363a2a359656dd20b28414de5d265"
integrity sha512-uGwPWlE0Hj972KkHtCDVwZ8O39GmyjfMane1Z3GUBGGnkZ2USDq7SxLpVIiIHpweY9DS0QTDH0Nw7RNBsAAZ5A==
"@types/spark-md5@^3.0.2":
version "3.0.2"
@ -223,10 +223,10 @@
"@typescript-eslint/types" "5.32.0"
eslint-visitor-keys "^3.3.0"
"@vitejs/plugin-vue@^3.1.2":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.1.2.tgz#3cd52114e8871a0b5e7bd7d837469c032e503036"
integrity sha512-3zxKNlvA3oNaKDYX0NBclgxTQ1xaFdL7PzwF6zj9tGFziKwmBa3Q/6XcJQxudlT81WxDjEhHmevvIC4Orc1LhQ==
"@vitejs/plugin-vue@^3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz#a1484089dd85d6528f435743f84cdd0d215bbb54"
integrity sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw==
"@volar/language-core@1.0.9":
version "1.0.9"
@ -785,10 +785,10 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-plugin-vue@^9.6.0:
version "9.6.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.6.0.tgz#5d1825b93d54595b1ba97106843e1d28cf3bb291"
integrity sha512-zzySkJgVbFCylnG2+9MDF7N+2Rjze2y0bF8GyUNpFOnT8mCMfqqtLDJkHBuYu9N/psW1A6DVbQhPkP92E+qakA==
eslint-plugin-vue@^9.7.0:
version "9.7.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.7.0.tgz#d391b9864f128ea2d1ee4dabeafb5f7c0cea981f"
integrity sha512-DrOO3WZCZEwcLsnd3ohFwqCoipGRSTKTBTnLwdhqAbYZtzWl0o7D+D8ZhlmiZvABKTEl8AFsqH1GHGdybyoQmw==
dependencies:
eslint-utils "^3.0.0"
natural-compare "^1.4.0"
@ -1633,7 +1633,7 @@ postcss@^8.1.10:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.16:
postcss@^8.4.18:
version "8.4.18"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.18.tgz#6d50046ea7d3d66a85e0e782074e7203bc7fbca2"
integrity sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==
@ -1706,10 +1706,10 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rollup@~2.78.0:
version "2.78.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.1.tgz#52fe3934d9c83cb4f7c4cb5fb75d88591be8648f"
integrity sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==
rollup@^2.79.1:
version "2.79.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
optionalDependencies:
fsevents "~2.3.2"
@ -1978,20 +1978,20 @@ vite-plugin-compression@^0.5.1:
debug "^4.3.3"
fs-extra "^10.0.0"
vite-plugin-css-injected-by-js@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-2.1.0.tgz#3c75c8f2eb41128f6fe22ca9e3eae6bde63231f0"
integrity sha512-p5Tn63xfYd1j4Hhnn9YuVMF3t7eLpiHejKNcIeGOWNU60bsw8WXmLQSVGtraw3C6qNz5LnF3S8R1SQLe4/LBYw==
vite-plugin-css-injected-by-js@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-2.1.1.tgz#a79275241c61f1c8d55d228f5b2dded450a580e4"
integrity sha512-gjIG6iFWde32oRr/bK9CsfN+jdbura2y4GlDzeOiEm6py38ud8dXzFl9zwmHjOjZdr8XEgQ9TovzVGNzp47esw==
vite@^3.1.8:
version "3.1.8"
resolved "https://registry.yarnpkg.com/vite/-/vite-3.1.8.tgz#fa29144167d19b773baffd65b3972ea4c12359c9"
integrity sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg==
vite@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.2.tgz#280762bfaf47bcea1d12698427331c0009ac7c1f"
integrity sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==
dependencies:
esbuild "^0.15.9"
postcss "^8.4.16"
postcss "^8.4.18"
resolve "^1.22.1"
rollup "~2.78.0"
rollup "^2.79.1"
optionalDependencies:
fsevents "~2.3.2"

Binary file not shown.