mirror of
https://github.com/lumapu/ahoy.git
synced 2025-05-12 08:26:38 +02:00
0.8.52
* possible fix of 'division by zero' #1345 * fix lang #1348 #1346 * fix timestamp `max AC power` #1324 * fix stylesheet overlay `max AC power` #1324 * fix download link #1340 * fix history graph * try to fix #1331
This commit is contained in:
parent
9f39e5c150
commit
455d29a6fa
16 changed files with 71 additions and 38 deletions
|
@ -1,5 +1,14 @@
|
||||||
# Development Changes
|
# Development Changes
|
||||||
|
|
||||||
|
## 0.8.52 - 2024-01-11
|
||||||
|
* possible fix of 'division by zero' #1345
|
||||||
|
* fix lang #1348 #1346
|
||||||
|
* fix timestamp `max AC power` #1324
|
||||||
|
* fix stylesheet overlay `max AC power` #1324
|
||||||
|
* fix download link #1340
|
||||||
|
* fix history graph
|
||||||
|
* try to fix #1331
|
||||||
|
|
||||||
## 0.8.51 - 2024-01-10
|
## 0.8.51 - 2024-01-10
|
||||||
* fix translation #1346
|
* fix translation #1346
|
||||||
* further improve sending active power control command faster #1332
|
* further improve sending active power control command faster #1332
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//-------------------------------------
|
//-------------------------------------
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 0
|
||||||
#define VERSION_MINOR 8
|
#define VERSION_MINOR 8
|
||||||
#define VERSION_PATCH 51
|
#define VERSION_PATCH 52
|
||||||
|
|
||||||
//-------------------------------------
|
//-------------------------------------
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
|
@ -961,8 +961,10 @@ static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) {
|
||||||
acMaxPower = iv->getValue(i, rec);
|
acMaxPower = iv->getValue(i, rec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(acPower > acMaxPower)
|
if(acPower > acMaxPower) {
|
||||||
|
iv->tsMaxAcPower = *iv->timestamp;
|
||||||
return acPower;
|
return acPower;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return acMaxPower;
|
return acMaxPower;
|
||||||
}
|
}
|
||||||
|
@ -981,10 +983,8 @@ static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) {
|
||||||
dcMaxPower = iv->getValue(i, rec);
|
dcMaxPower = iv->getValue(i, rec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(dcPower > dcMaxPower) {
|
if(dcPower > dcMaxPower)
|
||||||
iv->tsMaxAcPower = *iv->timestamp;
|
|
||||||
return dcPower;
|
return dcPower;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return dcMaxPower;
|
return dcMaxPower;
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,7 +166,10 @@ class DisplayMono {
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t sss2pgpos(uint seconds_since_start) {
|
uint8_t sss2pgpos(uint seconds_since_start) {
|
||||||
return(seconds_since_start * (mPgWidth - 1) / (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime));
|
uint32_t diff = (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime);
|
||||||
|
if(diff)
|
||||||
|
return (seconds_since_start * (mPgWidth - 1) / diff);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void calcPowerGraphValues() {
|
void calcPowerGraphValues() {
|
||||||
|
@ -175,6 +178,8 @@ class DisplayMono {
|
||||||
mPgTimeOfDay = (mDisplayData->utcTs > mDisplayData->pGraphStartTime) ? mDisplayData->utcTs - mDisplayData->pGraphStartTime : 0; // current time of day with respect to current sunrise time
|
mPgTimeOfDay = (mDisplayData->utcTs > mDisplayData->pGraphStartTime) ? mDisplayData->utcTs - mDisplayData->pGraphStartTime : 0; // current time of day with respect to current sunrise time
|
||||||
if (oldTimeOfDay > mPgTimeOfDay) // new day -> reset old data
|
if (oldTimeOfDay > mPgTimeOfDay) // new day -> reset old data
|
||||||
resetPowerGraph();
|
resetPowerGraph();
|
||||||
|
if(0 == mPgPeriod)
|
||||||
|
mPgPeriod = 1;
|
||||||
mPgLastPos = std::min((uint8_t) (mPgTimeOfDay * (mPgWidth - 1) / mPgPeriod), (uint8_t) (mPgWidth - 1)); // current datapoint based on currenct time of day
|
mPgLastPos = std::min((uint8_t) (mPgTimeOfDay * (mPgWidth - 1) / mPgPeriod), (uint8_t) (mPgWidth - 1)); // current datapoint based on currenct time of day
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,15 +195,13 @@ class DisplayMono {
|
||||||
uint8_t getPowerGraphXpos(uint8_t p) {
|
uint8_t getPowerGraphXpos(uint8_t p) {
|
||||||
if ((p <= mPgLastPos) && (mPgLastPos > 0))
|
if ((p <= mPgLastPos) && (mPgLastPos > 0))
|
||||||
return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis
|
return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis
|
||||||
else
|
return 0;
|
||||||
return(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t getPowerGraphYpos(uint8_t p) {
|
uint8_t getPowerGraphYpos(uint8_t p) {
|
||||||
if (p < mPgWidth)
|
if ((p < mPgWidth) && (mPgMaxPwr > 0))
|
||||||
return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height
|
return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height
|
||||||
else
|
return 0;
|
||||||
return(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void plotPowerGraph(uint8_t xoff, uint8_t yoff) {
|
void plotPowerGraph(uint8_t xoff, uint8_t yoff) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// 2023 Ahoy, https://ahoydtu.de
|
// 2024 Ahoy, https://ahoydtu.de
|
||||||
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ class DisplayMono128X32 : public DisplayMono {
|
||||||
void printText(const char *text, uint8_t line) {
|
void printText(const char *text, uint8_t line) {
|
||||||
setFont(line);
|
setFont(line);
|
||||||
|
|
||||||
uint8_t dispX = mLineXOffsets[line] + pixelShiftRange / 2 + mPixelshift;
|
uint8_t dispX = mLineXOffsets[line] + (pixelShiftRange / 2) + mPixelshift;
|
||||||
|
|
||||||
if (isTwoRowLine(line)) {
|
if (isTwoRowLine(line)) {
|
||||||
String stringText = String(text);
|
String stringText = String(text);
|
||||||
|
|
|
@ -193,13 +193,13 @@ class DisplayMono128X64 : public DisplayMono {
|
||||||
mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy);
|
mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy);
|
||||||
char sym[]=" ";
|
char sym[]=" ";
|
||||||
sym[0] = mDisplayData->RadioSymbol?'A':'E'; // NRF
|
sym[0] = mDisplayData->RadioSymbol?'A':'E'; // NRF
|
||||||
mDisplay->drawStr(widthShrink / 2 + mPixelshift, mLineYOffsets[l_RSSI], sym);
|
mDisplay->drawStr((widthShrink / 2) + mPixelshift, mLineYOffsets[l_RSSI], sym);
|
||||||
|
|
||||||
if (mDisplayData->MQTTSymbol)
|
if (mDisplayData->MQTTSymbol)
|
||||||
sym[0] = 'J'; // MQTT
|
sym[0] = 'J'; // MQTT
|
||||||
else
|
else
|
||||||
sym[0] = mDisplayData->WifiSymbol?'B':'F'; // Wifi
|
sym[0] = mDisplayData->WifiSymbol?'B':'F'; // Wifi
|
||||||
mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - widthShrink / 2 + mPixelshift, mLineYOffsets[l_RSSI], sym);
|
mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - (widthShrink / 2) + mPixelshift, mLineYOffsets[l_RSSI], sym);
|
||||||
mDisplay->sendBuffer();
|
mDisplay->sendBuffer();
|
||||||
|
|
||||||
mExtra++;
|
mExtra++;
|
||||||
|
@ -241,8 +241,8 @@ class DisplayMono128X64 : public DisplayMono {
|
||||||
mLineYOffsets[i] = yOff;
|
mLineYOffsets[i] = yOff;
|
||||||
dsc = mDisplay->getDescent();
|
dsc = mDisplay->getDescent();
|
||||||
yOff -= dsc;
|
yOff -= dsc;
|
||||||
if (l_Time == i) // prevent time and status line to touch
|
if (l_Time == i) // prevent time and status line to touch
|
||||||
yOff++; // -> one pixels space
|
yOff++; // -> one pixels space
|
||||||
i++;
|
i++;
|
||||||
} while(l_MAX_LINES>i);
|
} while(l_MAX_LINES>i);
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ class DisplayMono64X48 : public DisplayMono {
|
||||||
}
|
}
|
||||||
|
|
||||||
void printText(const char *text, uint8_t line) {
|
void printText(const char *text, uint8_t line) {
|
||||||
uint8_t dispX = mLineXOffsets[line] + pixelShiftRange/2 + mPixelshift;
|
uint8_t dispX = mLineXOffsets[line] + pixelShiftRange / 2 + mPixelshift;
|
||||||
|
|
||||||
setFont(line);
|
setFont(line);
|
||||||
mDisplay->drawStr(dispX, mLineYOffsets[line], text);
|
mDisplay->drawStr(dispX, mLineYOffsets[line], text);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// 2023 Ahoy, https://ahoydtu.de
|
// 2024 Ahoy, https://ahoydtu.de
|
||||||
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ class HistoryData {
|
||||||
uint16_t dispIdx; // index for 1st Element to display from WattArr
|
uint16_t dispIdx; // index for 1st Element to display from WattArr
|
||||||
bool wrapped;
|
bool wrapped;
|
||||||
// ring buffer for watt history
|
// ring buffer for watt history
|
||||||
std::array<uint16_t, HISTORY_DATA_ARR_LENGTH + 1> data;
|
std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data;
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
loopCnt = 0;
|
loopCnt = 0;
|
||||||
|
@ -78,13 +78,15 @@ class HistoryData {
|
||||||
mMaximumDay = roundf(maxPwr);
|
mMaximumDay = roundf(maxPwr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*mTs > mApp->getSunset()) {
|
if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) {
|
||||||
if ((!mDayStored) && (yldDay > 0)) {
|
if (*mTs > mApp->getSunset()) {
|
||||||
addValue(&mYieldDay, roundf(yldDay));
|
if ((!mDayStored) && (yldDay > 0)) {
|
||||||
mDayStored = true;
|
addValue(&mYieldDay, roundf(yldDay));
|
||||||
}
|
mDayStored = true;
|
||||||
} else if (*mTs > mApp->getSunrise())
|
}
|
||||||
mDayStored = false;
|
} else if (*mTs > mApp->getSunrise())
|
||||||
|
mDayStored = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t valueAt(HistoryStorageType type, uint16_t i) {
|
uint16_t valueAt(HistoryStorageType type, uint16_t i) {
|
||||||
|
|
|
@ -141,7 +141,7 @@ class PubMqttIvData {
|
||||||
|
|
||||||
// calculate total values for RealTimeRunData_Debug
|
// calculate total values for RealTimeRunData_Debug
|
||||||
if (CH0 == rec->assign[mPos].ch) {
|
if (CH0 == rec->assign[mPos].ch) {
|
||||||
if(mIv->getStatus() != InverterStatus::OFF) {
|
if(mIv->getStatus() > InverterStatus::OFF) {
|
||||||
if(mIv->config->add2Total) {
|
if(mIv->config->add2Total) {
|
||||||
mTotalFound = true;
|
mTotalFound = true;
|
||||||
switch (rec->assign[mPos].fieldId) {
|
switch (rec->assign[mPos].fieldId) {
|
||||||
|
|
|
@ -44,19 +44,23 @@
|
||||||
function parseHistory(obj, namePrefix, execOnce) {
|
function parseHistory(obj, namePrefix, execOnce) {
|
||||||
mRefresh = obj.refresh
|
mRefresh = obj.refresh
|
||||||
var data = Object.assign({}, obj.value)
|
var data = Object.assign({}, obj.value)
|
||||||
var numDataPts = data.length
|
numDataPts = Object.keys(data).length
|
||||||
|
|
||||||
if (true == execOnce) {
|
if (true == execOnce) {
|
||||||
let s = svg(null, (numDataPts + 2) * 2, mChartHeight, "chart");
|
let s = document.createElementNS(svgns, "svg");
|
||||||
|
s.setAttribute("class", "chart");
|
||||||
|
s.setAttribute("width", (numDataPts + 2) * 2);
|
||||||
|
s.setAttribute("height", mChartHeight);
|
||||||
s.setAttribute("role", "img");
|
s.setAttribute("role", "img");
|
||||||
|
|
||||||
let g = document.createElementNS(svgns, "g");
|
let g = document.createElementNS(svgns, "g");
|
||||||
s.appendChild(g);
|
s.appendChild(g);
|
||||||
for (var i = 0; i < numDataPts; i++) {
|
for (var i = 0; i < numDataPts; i++) {
|
||||||
val = data[i];
|
val = data[i];
|
||||||
let rect = document.createElementNS(svgns, "rect");
|
let rect = document.createElementNS(svgns, "rect");
|
||||||
rect.setAttribute("id", namePrefix+"Rect" + i);
|
rect.setAttribute("id", namePrefix+"Rect" + i);
|
||||||
rect.setAttribute("x", String(i * 2) + "");
|
rect.setAttribute("x", i * 2);
|
||||||
rect.setAttribute("width", String(2) + "");
|
rect.setAttribute("width", 2);
|
||||||
g.appendChild(rect);
|
g.appendChild(rect);
|
||||||
}
|
}
|
||||||
document.getElementById(namePrefix+"Chart").appendChild(s);
|
document.getElementById(namePrefix+"Chart").appendChild(s);
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<li>{#DISCUSS} <a href="https://discord.gg/WzhxEY62mB">Discord</a></li>
|
<li>{#DISCUSS} <a href="https://discord.gg/WzhxEY62mB">Discord</a></li>
|
||||||
<li>{#REPORT} <a href="https://github.com/lumapu/ahoy/issues" target="_blank">{#ISSUES}</a></li>
|
<li>{#REPORT} <a href="https://github.com/lumapu/ahoy/issues" target="_blank">{#ISSUES}</a></li>
|
||||||
<li>{#CONTRIBUTE} <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md" target="_blank">{#DOCUMENTATION}</a></li>
|
<li>{#CONTRIBUTE} <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md" target="_blank">{#DOCUMENTATION}</a></li>
|
||||||
<li><a href="https://fw.ahoydtu.de/dev/" target="_blank">Download</a> & Test {#DEV_FIRMWARE}, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">{#DEV_CHANGELOG}</a></li>
|
<li><a href="https://fw.ahoydtu.de/fw/dev/" target="_blank">Download</a> & Test {#DEV_FIRMWARE}, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">{#DEV_CHANGELOG}</a></li>
|
||||||
<li>{#DON_MAKE} <a href="https://paypal.me/lupusch" target="_blank">{#DONATION}</a></li>
|
<li>{#DON_MAKE} <a href="https://paypal.me/lupusch" target="_blank">{#DONATION}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -269,7 +269,7 @@
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12 col-sm-3 my-2">Discovery Config (homeassistant)</div>
|
<div class="col-12 col-sm-3 my-2">Discovery Config (homeassistant)</div>
|
||||||
<div class="col-12 col-sm-9">
|
<div class="col-12 col-sm-9">
|
||||||
<input type="button" name="mqttDiscovery" id="mqttDiscovery" class="btn" value="send" onclick="sendDiscoveryConfig()"/>
|
<input type="button" name="mqttDiscovery" id="mqttDiscovery" class="btn" value="{#BTN_SEND}" onclick="sendDiscoveryConfig()"/>
|
||||||
<span id="apiResultMqtt"></span>
|
<span id="apiResultMqtt"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -309,7 +309,7 @@
|
||||||
<div class="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div>
|
<div class="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div>
|
||||||
<div class="col-4 col-sm-9">
|
<div class="col-4 col-sm-9">
|
||||||
<input type="checkbox" name="reboot" checked />
|
<input type="checkbox" name="reboot" checked />
|
||||||
<input type="submit" value="save" class="btn right"/>
|
<input type="submit" value="{#BTN_SAVE}" class="btn right"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -666,7 +666,7 @@ div.hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.tooltip{
|
.tooltip:hover {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.tooltip:hover:after {
|
.tooltip:hover:after {
|
||||||
|
|
|
@ -138,7 +138,7 @@
|
||||||
ml("div", {class: "row mt-2"},[
|
ml("div", {class: "row mt-2"},[
|
||||||
numMid(obj.ch[0][11], "W", "{#MAX_AC_POWER}", {class: "fs-6 tooltip", data: maxAcPwr}),
|
numMid(obj.ch[0][11], "W", "{#MAX_AC_POWER}", {class: "fs-6 tooltip", data: maxAcPwr}),
|
||||||
numMid(obj.ch[0][8], "W", "{#DC_POWER}"),
|
numMid(obj.ch[0][8], "W", "{#DC_POWER}"),
|
||||||
numMid(obj.ch[0][0], "V", "{#DC_VOLTAGE}"),
|
numMid(obj.ch[0][0], "V", "{#AC_VOLTAGE}"),
|
||||||
numMid(obj.ch[0][1], "A", "{#AC_CURRENT}"),
|
numMid(obj.ch[0][1], "A", "{#AC_CURRENT}"),
|
||||||
numMid(obj.ch[0][3], "Hz", "{#FREQUENCY}"),
|
numMid(obj.ch[0][3], "Hz", "{#FREQUENCY}"),
|
||||||
numMid(obj.ch[0][9], "%", "{#EFFICIENCY}"),
|
numMid(obj.ch[0][9], "%", "{#EFFICIENCY}"),
|
||||||
|
@ -362,7 +362,7 @@
|
||||||
var v = getGridValue(glob);
|
var v = getGridValue(glob);
|
||||||
if(null === g) {
|
if(null === g) {
|
||||||
if(0 == obj.grid.length) {
|
if(0 == obj.grid.length) {
|
||||||
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#PROFILE_NOT_READ}?"))))
|
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#PROFILE_NOT_READ}"))))
|
||||||
} else {
|
} else {
|
||||||
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("h5", {}, "{#UNKNOWN_PROFILE}"))))
|
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("h5", {}, "{#UNKNOWN_PROFILE}"))))
|
||||||
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#OPEN_ISSUE}."))))
|
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#OPEN_ISSUE}."))))
|
||||||
|
|
|
@ -433,6 +433,16 @@
|
||||||
"en": "Line 1-4",
|
"en": "Line 1-4",
|
||||||
"de": "Zeile 1-4"
|
"de": "Zeile 1-4"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"token": "BTN_SAVE",
|
||||||
|
"en": "save",
|
||||||
|
"de": "speichern"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token": "BTN_SEND",
|
||||||
|
"en": "send",
|
||||||
|
"de": "senden"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"token": "BTN_REBOOT_SUCCESSFUL_SAVE",
|
"token": "BTN_REBOOT_SUCCESSFUL_SAVE",
|
||||||
"en": "Reboot device after successful save",
|
"en": "Reboot device after successful save",
|
||||||
|
@ -1118,6 +1128,11 @@
|
||||||
"en": "DC Voltage",
|
"en": "DC Voltage",
|
||||||
"de": "DC Spannung"
|
"de": "DC Spannung"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"token": "AC_VOLTAGE",
|
||||||
|
"en": "AC Voltage",
|
||||||
|
"de": "Netzspannung"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"token": "AC_CURRENT",
|
"token": "AC_CURRENT",
|
||||||
"en": "AC Current",
|
"en": "AC Current",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue