Skip to content

Commit

Permalink
Merge remote-tracking branch 'tbnobody/OpenDTU/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
helgeerbe committed Nov 3, 2022
2 parents 6b4129c + 72a773f commit b3295f5
Show file tree
Hide file tree
Showing 45 changed files with 331 additions and 194 deletions.
5 changes: 3 additions & 2 deletions docs/MQTT_Topics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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. | % |
Expand Down
2 changes: 1 addition & 1 deletion include/MqttHassPublishing.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 1 addition & 1 deletion include/MqttPublishing.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class MqttPublishingClass {
FLD_PAC,
FLD_F,
FLD_T,
FLD_PCT,
FLD_PF,
FLD_EFF,
FLD_IRR,
FLD_PRA
Expand Down
3 changes: 2 additions & 1 deletion include/WebApi_ws_live.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class WebApiWsLiveClass {

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

Expand Down
1 change: 1 addition & 0 deletions lib/Hoymiles/src/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* CommandAbstract
* DevControlCommand
* ActivePowerControlCommand
* PowerControlCommand
* MultiDataCommand
* AlarmDataCommand
* DevInfoAllCommand
Expand Down
4 changes: 2 additions & 2 deletions lib/Hoymiles/src/inverters/HM_1CH.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ class HM_1CH : public HM_Abstract {
{ 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 },
Expand Down
4 changes: 2 additions & 2 deletions lib/Hoymiles/src/inverters/HM_2CH.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ class HM_2CH : public HM_Abstract {
{ 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 },
Expand Down
4 changes: 2 additions & 2 deletions lib/Hoymiles/src/inverters/HM_4CH.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ class HM_4CH : public HM_Abstract {
{ 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 },
Expand Down
19 changes: 19 additions & 0 deletions lib/Hoymiles/src/parser/StatisticsParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions lib/Hoymiles/src/parser/StatisticsParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -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", "" };

Expand All @@ -32,7 +32,7 @@ enum {
FLD_PAC,
FLD_F,
FLD_T,
FLD_PCT,
FLD_PF,
FLD_EFF,
FLD_IRR,
FLD_PRA,
Expand Down Expand Up @@ -83,6 +83,7 @@ class StatisticsParser : public Parser {
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();

Expand Down
6 changes: 6 additions & 0 deletions src/MqttPublishing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
89 changes: 59 additions & 30 deletions src/WebApi_ws_live.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

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(JsonVariant& root, uint8_t idx, std::shared_ptr<InverterAbstract> inv, uint8_t channel, uint8_t fieldId, String topic)
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;
Expand All @@ -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) {
Expand All @@ -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);

Expand Down
10 changes: 5 additions & 5 deletions webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
2 changes: 1 addition & 1 deletion webapp/src/components/FirmwareInfo.vue
Original file line number Diff line number Diff line change
@@ -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">
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/components/HardwareInfo.vue
Original file line number Diff line number Diff line change
@@ -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">
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/components/InterfaceApInfo.vue
Original file line number Diff line number Diff line change
@@ -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">
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/components/InterfaceNetworkInfo.vue
Original file line number Diff line number Diff line change
@@ -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">
Expand Down
11 changes: 4 additions & 7 deletions webapp/src/components/InverterChannelInfo.vue
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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>
Expand All @@ -28,18 +28,15 @@
<script lang="ts">
import { defineComponent, type PropType } from 'vue';
import type { InverterStatistics } from '@/types/LiveDataStatus';
import { formatNumber } from '@/utils';
export default defineComponent({
props: {
channelData: { type: Object as PropType<InverterStatistics>, required: true },
channelNumber: { type: Number, required: true },
},
methods: {
formatNumber(num: number) {
return new Intl.NumberFormat(
undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }
).format(num);
},
formatNumber,
},
});
</script>
Loading

0 comments on commit b3295f5

Please sign in to comment.