diff --git a/README.md b/README.md index e217ebee..35b77bf4 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Sends text raw data as difined in VE.Direct spec. * Show inverters internal event log * Show inverter information like firmware version, firmware build date, hardware revision and hardware version * Show and set the current inverter limit +* Function to turn the inverter off an on * Uses ESP32 microcontroller and NRF24L01+ * Multi-Inverter support * MQTT support (with TLS) @@ -95,7 +96,7 @@ Sample Picture: Also supported: Board with Ethernet-Connector and Power-over-Ethernet [Olimex ESP32-POE](https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware) -#### NRF24L01+ radio board +### NRF24L01+ radio board The PLUS sign is IMPORTANT! There are different variants available, with antenna on the printed circuit board or external antenna. Sample picture: @@ -113,7 +114,7 @@ A heavily incomplete list of trusted hardware shops in germany is: This list is for your convenience only, the project is not related to any of these shops. -#### Power supply +### Power supply Use a power suppy with 5V and 1A. The USB cable connected to your PC/Notebook may be powerful enough or may be not. diff --git a/docs/MQTT_Topics.md b/docs/MQTT_Topics.md index e4c83024..6d9243c1 100644 --- a/docs/MQTT_Topics.md +++ b/docs/MQTT_Topics.md @@ -66,4 +66,5 @@ cmd topics are used to set values. Status topics are updated from values set in | [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. | % | -| [serial]/cmd/limit_nonpersistent_absolute | W | Set the inverter limit as a absolute value. 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 after around 4 minutes. | Watt (W) | \ No newline at end of file +| [serial]/cmd/limit_nonpersistent_absolute | W | Set the inverter limit as a absolute value. 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 after around 4 minutes. | Watt (W) | +| [serial]/cmd/power | W | Turn the inverter on (1) or off (0) | 0 or 1 | \ No newline at end of file diff --git a/include/WebApi.h b/include/WebApi.h index 55dc3238..6d59da71 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -11,6 +11,7 @@ #include "WebApi_mqtt.h" #include "WebApi_network.h" #include "WebApi_ntp.h" +#include "WebApi_power.h" #include "WebApi_sysstatus.h" #include "WebApi_webapp.h" #include "WebApi_ws_live.h" @@ -38,6 +39,7 @@ private: WebApiMqttClass _webApiMqtt; WebApiNetworkClass _webApiNetwork; WebApiNtpClass _webApiNtp; + WebApiPowerClass _webApiPower; WebApiSysstatusClass _webApiSysstatus; WebApiWebappClass _webApiWebapp; WebApiWsLiveClass _webApiWsLive; diff --git a/include/WebApi_power.h b/include/WebApi_power.h new file mode 100644 index 00000000..f8912c0f --- /dev/null +++ b/include/WebApi_power.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class WebApiPowerClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onPowerStatus(AsyncWebServerRequest* request); + void onPowerPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index ed2651fb..6107789d 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -46,8 +46,14 @@ void HoymilesClass::loop() iv->resendActivePowerControlRequest(_radio.get()); } + // Set power status if required + if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { + Serial.println(F("Resend PowerCommand")); + iv->resendPowerControlRequest(_radio.get()); + } + // Fetch dev info (but first fetch stats) - if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSample() == 0)) { + if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) { Serial.println(F("Request device info")); iv->sendDevInfoRequest(_radio.get()); } diff --git a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp index dd935e94..7b583c9a 100644 --- a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp +++ b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp @@ -28,6 +28,6 @@ bool DevInfoSimpleCommand::handleResponse(InverterAbstract* inverter, fragment_t inverter->DevInfo()->appendFragmentSimple(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter->DevInfo()->setLastUpdateSample(millis()); + inverter->DevInfo()->setLastUpdateSimple(millis()); return true; } \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/MultiDataCommand.cpp b/lib/Hoymiles/src/commands/MultiDataCommand.cpp index e752ea1d..92fe78b8 100644 --- a/lib/Hoymiles/src/commands/MultiDataCommand.cpp +++ b/lib/Hoymiles/src/commands/MultiDataCommand.cpp @@ -61,7 +61,7 @@ CommandAbstract* MultiDataCommand::getRequestFrameCommand(uint8_t frame_no) bool MultiDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) { // All fragments are available --> Check CRC - uint16_t crc = 0xffff, crcRcv; + uint16_t crc = 0xffff, crcRcv = 0; for (uint8_t i = 0; i < max_fragment_id; i++) { if (i == max_fragment_id - 1) { diff --git a/lib/Hoymiles/src/commands/PowerControlCommand.cpp b/lib/Hoymiles/src/commands/PowerControlCommand.cpp new file mode 100644 index 00000000..9bd66973 --- /dev/null +++ b/lib/Hoymiles/src/commands/PowerControlCommand.cpp @@ -0,0 +1,56 @@ +#include "PowerControlCommand.h" +#include "inverters/InverterAbstract.h" + +#define CRC_SIZE 2 + +PowerControlCommand::PowerControlCommand(uint64_t target_address, uint64_t router_address) + : DevControlCommand(target_address, router_address) +{ + _payload[10] = 0x00; // TurnOn + _payload[11] = 0x00; + + udpateCRC(CRC_SIZE); // 2 byte crc + + _payload_size = 14; + + setTimeout(2000); +} + +String PowerControlCommand::getCommandName() +{ + return "PowerControl"; +} + +bool PowerControlCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +{ + if (!DevControlCommand::handleResponse(inverter, fragment, max_fragment_id)) { + return false; + } + + inverter->PowerCommand()->setLastUpdateCommand(millis()); + inverter->PowerCommand()->setLastPowerCommandSuccess(CMD_OK); + return true; +} + +void PowerControlCommand::gotTimeout(InverterAbstract* inverter) +{ + inverter->PowerCommand()->setLastPowerCommandSuccess(CMD_NOK); +} + +void PowerControlCommand::setPowerOn(bool state) +{ + if (state) { + _payload[10] = 0x00; // TurnOn + } else { + _payload[10] = 0x01; // TurnOff + } + + udpateCRC(CRC_SIZE); // 2 byte crc +} + +void PowerControlCommand::setRestart() +{ + _payload[10] = 0x02; // Restart + + udpateCRC(CRC_SIZE); // 2 byte crc +} \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/PowerControlCommand.h b/lib/Hoymiles/src/commands/PowerControlCommand.h new file mode 100644 index 00000000..3b022565 --- /dev/null +++ b/lib/Hoymiles/src/commands/PowerControlCommand.h @@ -0,0 +1,16 @@ +#pragma once + +#include "DevControlCommand.h" + +class PowerControlCommand : public DevControlCommand { +public: + explicit PowerControlCommand(uint64_t target_address = 0, uint64_t router_address = 0); + + virtual String getCommandName(); + + virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + virtual void gotTimeout(InverterAbstract* inverter); + + void setPowerOn(bool state); + void setRestart(); +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index a4636bff..079c8642 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -4,6 +4,7 @@ #include "commands/AlarmDataCommand.h" #include "commands/DevInfoAllCommand.h" #include "commands/DevInfoSimpleCommand.h" +#include "commands/PowerControlCommand.h" #include "commands/RealTimeRunDataCommand.h" #include "commands/SystemConfigParaCommand.h" @@ -69,9 +70,9 @@ bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio) cmdAll->setTime(now); cmdAll->setTargetAddress(serial()); - DevInfoSimpleCommand* cmdSample = radio->enqueCommand(); - cmdSample->setTime(now); - cmdSample->setTargetAddress(serial()); + DevInfoSimpleCommand* cmdSimple = radio->enqueCommand(); + cmdSimple->setTime(now); + cmdSimple->setTargetAddress(serial()); return true; } @@ -110,4 +111,21 @@ bool HM_Abstract::sendActivePowerControlRequest(HoymilesRadio* radio, float limi bool HM_Abstract::resendActivePowerControlRequest(HoymilesRadio* radio) { return sendActivePowerControlRequest(radio, _activePowerControlLimit, _activePowerControlType); +} + +bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) +{ + _powerState = turnOn; + + PowerControlCommand* cmd = radio->enqueCommand(); + cmd->setPowerOn(turnOn); + cmd->setTargetAddress(serial()); + PowerCommand()->setLastPowerCommandSuccess(CMD_PENDING); + + return true; +} + +bool HM_Abstract::resendPowerControlRequest(HoymilesRadio* radio) +{ + return sendPowerControlRequest(radio, _powerState); } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.h b/lib/Hoymiles/src/inverters/HM_Abstract.h index 518af834..8cd07562 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.h +++ b/lib/Hoymiles/src/inverters/HM_Abstract.h @@ -11,9 +11,13 @@ public: bool sendSystemConfigParaRequest(HoymilesRadio* radio); bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type); bool resendActivePowerControlRequest(HoymilesRadio* radio); + bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn); + bool resendPowerControlRequest(HoymilesRadio* radio); private: uint8_t _lastAlarmLogCnt = 0; float _activePowerControlLimit = 0; PowerLimitControlType _activePowerControlType = PowerLimitControlType::AbsolutNonPersistent; + + bool _powerState = true; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index d6c1f01d..adb3191e 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -7,6 +7,7 @@ InverterAbstract::InverterAbstract(uint64_t serial) _serial.u64 = serial; _alarmLogParser.reset(new AlarmLogParser()); _devInfoParser.reset(new DevInfoParser()); + _powerCommandParser.reset(new PowerCommandParser()); _statisticsParser.reset(new StatisticsParser()); _systemConfigParaParser.reset(new SystemConfigParaParser()); } @@ -64,6 +65,11 @@ DevInfoParser* InverterAbstract::DevInfo() return _devInfoParser.get(); } +PowerCommandParser* InverterAbstract::PowerCommand() +{ + return _powerCommandParser.get(); +} + StatisticsParser* InverterAbstract::Statistics() { return _statisticsParser.get(); diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index bd53f9e2..4b9a2593 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -3,6 +3,7 @@ #include "../commands/ActivePowerControlCommand.h" #include "../parser/AlarmLogParser.h" #include "../parser/DevInfoParser.h" +#include "../parser/PowerCommandParser.h" #include "../parser/StatisticsParser.h" #include "../parser/SystemConfigParaParser.h" #include "HoymilesRadio.h" @@ -51,9 +52,12 @@ public: virtual bool sendSystemConfigParaRequest(HoymilesRadio* radio) = 0; virtual bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type) = 0; virtual bool resendActivePowerControlRequest(HoymilesRadio* radio) = 0; + virtual bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) = 0; + virtual bool resendPowerControlRequest(HoymilesRadio* radio) = 0; AlarmLogParser* EventLog(); DevInfoParser* DevInfo(); + PowerCommandParser* PowerCommand(); StatisticsParser* Statistics(); SystemConfigParaParser* SystemConfigPara(); @@ -67,6 +71,7 @@ private: std::unique_ptr _alarmLogParser; std::unique_ptr _devInfoParser; + std::unique_ptr _powerCommandParser; std::unique_ptr _statisticsParser; std::unique_ptr _systemConfigParaParser; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.cpp b/lib/Hoymiles/src/parser/AlarmLogParser.cpp index d3d06096..a5d09366 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.cpp +++ b/lib/Hoymiles/src/parser/AlarmLogParser.cpp @@ -40,12 +40,12 @@ void AlarmLogParser::getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry) uint32_t wcode = (uint16_t)_payloadAlarmLog[entryStartOffset] << 8 | _payloadAlarmLog[entryStartOffset + 1]; uint32_t startTimeOffset = 0; - if ((wcode >> 13) & 0x01 == 1) { + if (((wcode >> 13) & 0x01) == 1) { startTimeOffset = 12 * 60 * 60; } uint32_t endTimeOffset = 0; - if ((wcode >> 12) & 0x01 == 1) { + if (((wcode >> 12) & 0x01) == 1) { endTimeOffset = 12 * 60 * 60; } diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index 419dab03..ce32bfc7 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -26,7 +26,7 @@ void DevInfoParser::clearBufferSimple() void DevInfoParser::appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8_t len) { if (offset + len > DEV_INFO_SIZE) { - Serial.printf("FATAL: (%s, %d) dev info Sample packet too large for buffer\n", __FILE__, __LINE__); + Serial.printf("FATAL: (%s, %d) dev info Simple packet too large for buffer\n", __FILE__, __LINE__); return; } memcpy(&_payloadDevInfoSimple[offset], payload, len); @@ -44,14 +44,14 @@ void DevInfoParser::setLastUpdateAll(uint32_t lastUpdate) setLastUpdate(lastUpdate); } -uint32_t DevInfoParser::getLastUpdateSample() +uint32_t DevInfoParser::getLastUpdateSimple() { - return _lastUpdateSample; + return _lastUpdateSimple; } -void DevInfoParser::setLastUpdateSample(uint32_t lastUpdate) +void DevInfoParser::setLastUpdateSimple(uint32_t lastUpdate) { - _lastUpdateSample = lastUpdate; + _lastUpdateSimple = lastUpdate; setLastUpdate(lastUpdate); } @@ -90,9 +90,11 @@ uint32_t DevInfoParser::getHwPartNumber() return ((uint32_t)hwpn_h << 16) | ((uint32_t)hwpn_l); } -uint16_t DevInfoParser::getHwVersion() +String DevInfoParser::getHwVersion() { - return (((uint16_t)_payloadDevInfoSimple[6]) << 8) | _payloadDevInfoSimple[7]; + char buf[6]; + snprintf(buf, sizeof(buf), "%02X.%02X", _payloadDevInfoSimple[6], _payloadDevInfoSimple[7]); + return String(buf); } /* struct tm to seconds since Unix epoch */ diff --git a/lib/Hoymiles/src/parser/DevInfoParser.h b/lib/Hoymiles/src/parser/DevInfoParser.h index de0c1773..46065861 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.h +++ b/lib/Hoymiles/src/parser/DevInfoParser.h @@ -15,21 +15,21 @@ public: uint32_t getLastUpdateAll(); void setLastUpdateAll(uint32_t lastUpdate); - uint32_t getLastUpdateSample(); - void setLastUpdateSample(uint32_t lastUpdate); + uint32_t getLastUpdateSimple(); + void setLastUpdateSimple(uint32_t lastUpdate); uint16_t getFwBuildVersion(); time_t getFwBuildDateTime(); uint16_t getFwBootloaderVersion(); uint32_t getHwPartNumber(); - uint16_t getHwVersion(); + String getHwVersion(); private: time_t timegm(struct tm* tm); uint32_t _lastUpdateAll = 0; - uint32_t _lastUpdateSample = 0; + uint32_t _lastUpdateSimple = 0; uint8_t _payloadDevInfoAll[DEV_INFO_SIZE] = {}; uint8_t _devInfoAllLength = 0; diff --git a/lib/Hoymiles/src/parser/PowerCommandParser.cpp b/lib/Hoymiles/src/parser/PowerCommandParser.cpp new file mode 100644 index 00000000..1808660e --- /dev/null +++ b/lib/Hoymiles/src/parser/PowerCommandParser.cpp @@ -0,0 +1,22 @@ +#include "PowerCommandParser.h" + +void PowerCommandParser::setLastPowerCommandSuccess(LastCommandSuccess status) +{ + _lastLimitCommandSuccess = status; +} + +LastCommandSuccess PowerCommandParser::getLastPowerCommandSuccess() +{ + return _lastLimitCommandSuccess; +} + +uint32_t PowerCommandParser::getLastUpdateCommand() +{ + return _lastUpdateCommand; +} + +void PowerCommandParser::setLastUpdateCommand(uint32_t lastUpdate) +{ + _lastUpdateCommand = lastUpdate; + setLastUpdate(lastUpdate); +} \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/PowerCommandParser.h b/lib/Hoymiles/src/parser/PowerCommandParser.h new file mode 100644 index 00000000..c0ba6bec --- /dev/null +++ b/lib/Hoymiles/src/parser/PowerCommandParser.h @@ -0,0 +1,16 @@ +#pragma once +#include "Parser.h" +#include + +class PowerCommandParser : public Parser { +public: + void setLastPowerCommandSuccess(LastCommandSuccess status); + LastCommandSuccess getLastPowerCommandSuccess(); + uint32_t getLastUpdateCommand(); + void setLastUpdateCommand(uint32_t lastUpdate); + +private: + LastCommandSuccess _lastLimitCommandSuccess = CMD_OK; // Set to OK because we have to assume nothing is done at startup + + uint32_t _lastUpdateCommand = 0; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index ad02a660..1789eeeb 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -1,5 +1,28 @@ #include "StatisticsParser.h" +static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0); +static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0); +static float calcUdcCh(StatisticsParser* iv, uint8_t arg0); +static float calcPowerDcCh0(StatisticsParser* iv, uint8_t arg0); +static float calcEffiencyCh0(StatisticsParser* iv, uint8_t arg0); +static float calcIrradiation(StatisticsParser* iv, uint8_t arg0); + +using func_t = float(StatisticsParser*, uint8_t); + +struct calcFunc_t { + uint8_t funcId; // unique id + func_t* func; // function pointer +}; + +const calcFunc_t calcFunctions[] = { + { CALC_YT_CH0, &calcYieldTotalCh0 }, + { CALC_YD_CH0, &calcYieldDayCh0 }, + { CALC_UDC_CH, &calcUdcCh }, + { CALC_PDC_CH0, &calcPowerDcCh0 }, + { CALC_EFF_CH0, &calcEffiencyCh0 }, + { CALC_IRR_CH, &calcIrradiation } +}; + void StatisticsParser::setByteAssignment(const byteAssign_t* byteAssignment, const uint8_t count) { _byteAssignment = byteAssignment; diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index f1e7e4f5..d21f82e6 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -70,31 +70,6 @@ typedef struct { uint16_t div; // divisor / calc command } byteAssign_t; -// prototypes -class StatisticsParser; -static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0); -static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0); -static float calcUdcCh(StatisticsParser* iv, uint8_t arg0); -static float calcPowerDcCh0(StatisticsParser* iv, uint8_t arg0); -static float calcEffiencyCh0(StatisticsParser* iv, uint8_t arg0); -static float calcIrradiation(StatisticsParser* iv, uint8_t arg0); - -using func_t = float(StatisticsParser*, uint8_t); - -struct calcFunc_t { - uint8_t funcId; // unique id - func_t* func; // function pointer -}; - -const calcFunc_t calcFunctions[] = { - { CALC_YT_CH0, &calcYieldTotalCh0 }, - { CALC_YD_CH0, &calcYieldDayCh0 }, - { CALC_UDC_CH, &calcUdcCh }, - { CALC_PDC_CH0, &calcPowerDcCh0 }, - { CALC_EFF_CH0, &calcEffiencyCh0 }, - { CALC_IRR_CH, &calcIrradiation } -}; - class StatisticsParser : public Parser { public: void clearBuffer(); diff --git a/platformio.ini b/platformio.ini index e3316bba..f5e5b3da 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,16 +13,17 @@ default_envs = generic [env] framework = arduino -platform = espressif32@>=5 +platform = espressif32@>=5.2.0 build_flags = -D=${PIOENV} -DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/js/app.js.gz + -Wall lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer bblanchon/ArduinoJson @ ^6.19.4 - https://github.com/bertmelis/espMqttClient.git#v1.3.1 + https://github.com/bertmelis/espMqttClient.git#v1.3.2 nrf24/RF24 @ ^1.4.5 extra_scripts = diff --git a/src/MqttHassPublishing.cpp b/src/MqttHassPublishing.cpp index c18829b0..aaa50d9d 100644 --- a/src/MqttHassPublishing.cpp +++ b/src/MqttHassPublishing.cpp @@ -74,7 +74,7 @@ void MqttHassPublishingClass::publishField(std::shared_ptr inv } char serial[sizeof(uint64_t) * 8 + 1]; - snprintf(serial, sizeof(serial), "%0lx%08lx", + snprintf(serial, sizeof(serial), "%0x%08x", ((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)), ((uint32_t)(inv->serial() & 0xFFFFFFFF))); diff --git a/src/MqttPublishing.cpp b/src/MqttPublishing.cpp index 048a635e..9d569012 100644 --- a/src/MqttPublishing.cpp +++ b/src/MqttPublishing.cpp @@ -34,7 +34,7 @@ void MqttPublishingClass::loop() auto inv = Hoymiles.getInverterByPos(i); char buffer[sizeof(uint64_t) * 8 + 1]; - snprintf(buffer, sizeof(buffer), "%0lx%08lx", + snprintf(buffer, sizeof(buffer), "%0x%08x", ((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)), ((uint32_t)(inv->serial() & 0xFFFFFFFF))); String subtopic = String(buffer); @@ -59,7 +59,7 @@ void MqttPublishingClass::loop() MqttSettings.publish(subtopic + "/device/hwpartnumber", String(inv->DevInfo()->getHwPartNumber())); // Hardware version - MqttSettings.publish(subtopic + "/device/hwversion", String(inv->DevInfo()->getHwVersion())); + MqttSettings.publish(subtopic + "/device/hwversion", inv->DevInfo()->getHwVersion()); } if (inv->SystemConfigPara()->getLastUpdate() > 0) { @@ -106,7 +106,7 @@ String MqttPublishingClass::getTopic(std::shared_ptr inv, uint } char buffer[sizeof(uint64_t) * 8 + 1]; - snprintf(buffer, sizeof(buffer), "%0lx%08lx", + snprintf(buffer, sizeof(buffer), "%0x%08x", ((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)), ((uint32_t)(inv->serial() & 0xFFFFFFFF))); String invSerial = String(buffer); diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index e571d6ec..a874176e 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -14,6 +14,7 @@ #define TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE "limit_persistent_absolute" #define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative" #define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute" +#define TOPIC_SUB_POWER "power" MqttSettingsClass::MqttSettingsClass() { @@ -30,6 +31,8 @@ void MqttSettingsClass::NetworkEvent(network_event event) Serial.println(F("Network lost connection")); mqttReconnectTimer.detach(); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi break; + default: + break; } } @@ -44,6 +47,7 @@ void MqttSettingsClass::onMqttConnect(bool sessionPresent) mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE).c_str(), 0); mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE).c_str(), 0); mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE).c_str(), 0); + mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER).c_str(), 0); } void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) @@ -118,38 +122,43 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie char* strlimit = new char[len + 1]; memcpy(strlimit, payload, len); strlimit[len] = '\0'; - uint32_t limit = strtol(strlimit, NULL, 10); + uint32_t payload_val = strtol(strlimit, NULL, 10); delete[] strlimit; if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) { // Set inverter limit relative persistent - limit = min(100, limit); - Serial.printf("Limit Persistent: %d %%\n", limit); - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), limit, PowerLimitControlType::RelativPersistent); + payload_val = min(100, payload_val); + Serial.printf("Limit Persistent: %d %%\n", payload_val); + inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativPersistent); } else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) { // Set inverter limit absolute persistent - Serial.printf("Limit Persistent: %d W\n", limit); - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), limit, PowerLimitControlType::AbsolutPersistent); + Serial.printf("Limit Persistent: %d W\n", payload_val); + inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::AbsolutPersistent); } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) { // Set inverter limit relative non persistent - limit = min(100, limit); - Serial.printf("Limit Non-Persistent: %d %%\n", limit); + payload_val = min(100, payload_val); + Serial.printf("Limit Non-Persistent: %d %%\n", payload_val); if (!properties.retain) { - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), limit, PowerLimitControlType::RelativNonPersistent); + inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativNonPersistent); } else { Serial.println("Ignored because retained"); } } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)) { // Set inverter limit absolute non persistent - Serial.printf("Limit Non-Persistent: %d W\n", limit); + Serial.printf("Limit Non-Persistent: %d W\n", payload_val); if (!properties.retain) { - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), limit, PowerLimitControlType::AbsolutNonPersistent); + inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::AbsolutNonPersistent); } else { Serial.println("Ignored because retained"); } + + } else if(!strcmp(setting, TOPIC_SUB_POWER)) { + // Turn inverter on or off + Serial.printf("Set inverter power to: %d\n", payload_val); + inv->sendPowerControlRequest(Hoymiles.getRadio(), payload_val > 0); } } diff --git a/src/WebApi.cpp b/src/WebApi.cpp index e0df086d..91aa936c 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -27,6 +27,7 @@ void WebApiClass::init() _webApiMqtt.init(&_server); _webApiNetwork.init(&_server); _webApiNtp.init(&_server); + _webApiPower.init(&_server); _webApiSysstatus.init(&_server); _webApiWebapp.init(&_server); _webApiWsLive.init(&_server); @@ -48,6 +49,7 @@ void WebApiClass::loop() _webApiMqtt.loop(); _webApiNetwork.loop(); _webApiNtp.loop(); + _webApiPower.loop(); _webApiSysstatus.loop(); _webApiWebapp.loop(); _webApiWsLive.loop(); diff --git a/src/WebApi_devinfo.cpp b/src/WebApi_devinfo.cpp index e4a75a2e..c9a18436 100644 --- a/src/WebApi_devinfo.cpp +++ b/src/WebApi_devinfo.cpp @@ -31,11 +31,12 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request) // Inverter Serial is read as HEX char buffer[sizeof(uint64_t) * 8 + 1]; - snprintf(buffer, sizeof(buffer), "%0lx%08lx", + snprintf(buffer, sizeof(buffer), "%0x%08x", ((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)), ((uint32_t)(inv->serial() & 0xFFFFFFFF))); JsonObject devInfoObj = root[buffer].createNestedObject(); + devInfoObj[F("valid_data")] = inv->DevInfo()->getLastUpdate() > 0; devInfoObj[F("fw_bootloader_version")] = inv->DevInfo()->getFwBootloaderVersion(); devInfoObj[F("fw_build_version")] = inv->DevInfo()->getFwBuildVersion(); devInfoObj[F("hw_part_number")] = inv->DevInfo()->getHwPartNumber(); diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index 65c68580..b832c26d 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -30,7 +30,7 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) // DTU Serial is read as HEX char buffer[sizeof(uint64_t) * 8 + 1]; - snprintf(buffer, sizeof(buffer), "%0lx%08lx", + snprintf(buffer, sizeof(buffer), "%0x%08x", ((uint32_t)((config.Dtu_Serial >> 32) & 0xFFFFFFFF)), ((uint32_t)(config.Dtu_Serial & 0xFFFFFFFF))); root[F("dtu_serial")] = buffer; diff --git a/src/WebApi_eventlog.cpp b/src/WebApi_eventlog.cpp index b73b72ed..edff3bb6 100644 --- a/src/WebApi_eventlog.cpp +++ b/src/WebApi_eventlog.cpp @@ -36,7 +36,7 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) if (inv != nullptr) { // Inverter Serial is read as HEX char buffer[sizeof(uint64_t) * 8 + 1]; - snprintf(buffer, sizeof(buffer), "%0lx%08lx", + snprintf(buffer, sizeof(buffer), "%0x%08x", ((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)), ((uint32_t)(inv->serial() & 0xFFFFFFFF))); diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 411c4c7a..12cadaf0 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -42,7 +42,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) // Inverter Serial is read as HEX char buffer[sizeof(uint64_t) * 8 + 1]; - snprintf(buffer, sizeof(buffer), "%0lx%08lx", + snprintf(buffer, sizeof(buffer), "%0x%08x", ((uint32_t)((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF)), ((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF))); obj[F("serial")] = buffer; @@ -210,11 +210,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } JsonArray maxPowerArray = root[F("max_power")].as(); - uint8_t arrayCount = 0; - for (JsonVariant maxPower : maxPowerArray) { - arrayCount++; - } - if (arrayCount != INV_MAX_CHAN_COUNT) { + if (maxPowerArray.size() != INV_MAX_CHAN_COUNT) { retMsg[F("message")] = F("Invalid amount of max channel setting given!"); response->setLength(); request->send(response); @@ -230,7 +226,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inverter.Serial = new_serial; strncpy(inverter.Name, root[F("name")].as().c_str(), INV_MAX_NAME_STRLEN); - arrayCount = 0; + uint8_t arrayCount = 0; for (JsonVariant maxPower : maxPowerArray) { inverter.MaxChannelPower[arrayCount] = maxPower.as(); arrayCount++; @@ -319,7 +315,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) Hoymiles.removeInverterBySerial(inverter.Serial); inverter.Serial = 0; - strncpy(inverter.Name, "", 0); + strncpy(inverter.Name, "", sizeof(inverter.Name)); Configuration.write(); retMsg[F("type")] = F("success"); diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index af9a3779..56a3f0fc 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -31,7 +31,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) // Inverter Serial is read as HEX char buffer[sizeof(uint64_t) * 8 + 1]; - snprintf(buffer, sizeof(buffer), "%0lx%08lx", + snprintf(buffer, sizeof(buffer), "%0x%08x", ((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)), ((uint32_t)(inv->serial() & 0xFFFFFFFF))); diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp new file mode 100644 index 00000000..30aacf80 --- /dev/null +++ b/src/WebApi_power.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "WebApi_power.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Hoymiles.h" + +void WebApiPowerClass::init(AsyncWebServer* server) +{ + using std::placeholders::_1; + + _server = server; + + _server->on("/api/power/status", HTTP_GET, std::bind(&WebApiPowerClass::onPowerStatus, this, _1)); + _server->on("/api/power/config", HTTP_POST, std::bind(&WebApiPowerClass::onPowerPost, this, _1)); +} + +void WebApiPowerClass::loop() +{ +} + +void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + + // Inverter Serial is read as HEX + char buffer[sizeof(uint64_t) * 8 + 1]; + snprintf(buffer, sizeof(buffer), "%0x%08x", + ((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)), + ((uint32_t)(inv->serial() & 0xFFFFFFFF))); + + LastCommandSuccess status = inv->PowerCommand()->getLastPowerCommandSuccess(); + String limitStatus = "Unknown"; + if (status == LastCommandSuccess::CMD_OK) { + limitStatus = "Ok"; + } + else if (status == LastCommandSuccess::CMD_NOK) { + limitStatus = "Failure"; + } + else if (status == LastCommandSuccess::CMD_PENDING) { + limitStatus = "Pending"; + } + root[buffer]["power_set_status"] = limitStatus; + } + + response->setLength(); + request->send(response); +} + +void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > 1024) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(1024); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("serial") + && root.containsKey("power"))) { + retMsg[F("message")] = F("Values are missing!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("serial")].as() == 0) { + retMsg[F("message")] = F("Serial must be a number > 0!"); + response->setLength(); + request->send(response); + return; + } + + uint64_t serial = strtoll(root[F("serial")].as().c_str(), NULL, 16); + uint16_t power = root[F("power")].as(); + + auto inv = Hoymiles.getInverterBySerial(serial); + if (inv == nullptr) { + retMsg[F("message")] = F("Invalid inverter specified!"); + response->setLength(); + request->send(response); + return; + } + + inv->sendPowerControlRequest(Hoymiles.getRadio(), power); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Settings saved!"); + + response->setLength(); + request->send(response); +} \ No newline at end of file diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index 13864348..1500a6da 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -59,7 +59,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) char version[16]; snprintf(version, sizeof(version), "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); - root[F("firmware_version")] = version; + root[F("config_version")] = version; root[F("git_hash")] = AUTO_GIT_HASH; root[F("uptime")] = esp_timer_get_time() / 1000000; diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 6764be9c..ab098fac 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -78,7 +78,7 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) auto inv = Hoymiles.getInverterByPos(i); char buffer[sizeof(uint64_t) * 8 + 1]; - snprintf(buffer, sizeof(buffer), "%0lx%08lx", + snprintf(buffer, sizeof(buffer), "%0x%08x", ((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)), ((uint32_t)(inv->serial() & 0xFFFFFFFF))); diff --git a/webapp/package.json b/webapp/package.json index 44b99ae2..4f5d9b92 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -9,21 +9,21 @@ }, "dependencies": { "@popperjs/core": "^2.11.6", - "bootstrap": "^5.2.1", + "bootstrap": "^5.2.2", "bootstrap-icons-vue": "^1.8.1", - "core-js": "^3.25.3", + "core-js": "^3.25.5", "spark-md5": "^3.0.2", - "vue": "^3.2.39", + "vue": "^3.2.40", "vue-class-component": "^8.0.0-0", "vue-router": "^4.1.5" }, "devDependencies": { - "@babel/core": "^7.19.1", + "@babel/core": "^7.19.3", "@babel/eslint-parser": "^7.19.1", - "@types/bootstrap": "^5.2.4", - "@types/node": "^18.7.21", + "@types/bootstrap": "^5.2.5", + "@types/node": "^18.8.2", "@types/spark-md5": "^3.0.2", - "@typescript-eslint/parser": "^5.37.0", + "@typescript-eslint/parser": "^5.38.1", "@vue/cli-plugin-babel": "~5.0.8", "@vue/cli-plugin-eslint": "~5.0.8", "@vue/cli-plugin-router": "^5.0.6", @@ -31,8 +31,8 @@ "@vue/cli-service": "~5.0.8", "@vue/eslint-config-typescript": "^11.0.2", "eslint": "^8.24.0", - "eslint-plugin-vue": "^9.5.1", - "typescript": "^4.8.3", + "eslint-plugin-vue": "^9.6.0", + "typescript": "^4.8.4", "vue-cli-plugin-compression": "~2.0.0" }, "eslintConfig": { diff --git a/webapp/src/components/HomeView.vue b/webapp/src/components/HomeView.vue index 2412948e..a854f149 100644 --- a/webapp/src/components/HomeView.vue +++ b/webapp/src/components/HomeView.vue @@ -52,6 +52,14 @@ +
+ +
+
+ + @@ -292,6 +353,14 @@ export default defineComponent({ alertMessageLimit: "", alertTypeLimit: "info", showAlertLimit: false, + + powerSettingView: {} as bootstrap.Modal, + powerSettingSerial: 0, + powerSettingLoading: true, + alertMessagePower: "", + alertTypePower: "info", + showAlertPower: false, + successCommandPower: "", }; }, created() { @@ -303,8 +372,10 @@ export default defineComponent({ this.eventLogView = new bootstrap.Modal('#eventView'); this.devInfoView = new bootstrap.Modal('#devInfoView'); this.limitSettingView = new bootstrap.Modal('#limitSettingView'); + this.powerSettingView = new bootstrap.Modal('#powerSettingView'); (this.$refs.limitSettingView as HTMLElement).addEventListener("hide.bs.modal", this.onHideLimitSettings); + (this.$refs.powerSettingView as HTMLElement).addEventListener("hide.bs.modal", this.onHidePowerSettings); }, unmounted() { this.closeSocket(); @@ -483,6 +554,57 @@ export default defineComponent({ } this.targetLimitType = type; }, + + onShowPowerSettings(serial: number) { + this.powerSettingLoading = true; + fetch("/api/power/status") + .then((response) => response.json()) + .then((data) => { + this.successCommandPower = data[serial].power_set_status; + this.powerSettingSerial = serial; + this.powerSettingLoading = false; + }); + this.powerSettingView.show(); + }, + + onHidePowerSettings() { + this.powerSettingSerial = 0; + this.showAlertPower = false; + }, + + onSetPowerSettings(turnOn: boolean) { + const data = { + serial: this.powerSettingSerial, + power: turnOn, + }; + const formData = new FormData(); + formData.append("data", JSON.stringify(data)); + + console.log(data); + + fetch("/api/power/config", { + method: "POST", + body: formData, + }) + .then(function (response) { + if (response.status != 200) { + throw response.status; + } else { + return response.json(); + } + }) + .then( + (response) => { + if (response.type == "success") { + this.powerSettingView.hide(); + } else { + this.alertMessagePower = response.message; + this.alertTypePower = response.type; + this.showAlertPower = true; + } + } + ) + }, }, }); \ No newline at end of file diff --git a/webapp/src/components/SystemInfoView.vue b/webapp/src/components/SystemInfoView.vue index 55acd750..0e73c57e 100644 --- a/webapp/src/components/SystemInfoView.vue +++ b/webapp/src/components/SystemInfoView.vue @@ -45,12 +45,15 @@ export default defineComponent({ // FirmwareInfo hostname: "", sdkversion: "", - firmware_version: "", + config_version: "", git_hash: "", resetreason_0: "", resetreason_1: "", cfgsavecount: 0, uptime: 0, + update_text: "", + update_url: "", + update_status: "", // MemoryInfo heap_total: 0, heap_used: 0, @@ -72,8 +75,35 @@ export default defineComponent({ .then((data) => { this.systemDataList = data; this.dataLoading = false; + this.getUpdateInfo(); }) }, + getUpdateInfo() { + const fetchUrl = "https://api.github.com/repos/tbnobody/OpenDTU/compare/" + + this.systemDataList.git_hash?.substring(1) + "...HEAD"; + + fetch(fetchUrl) + .then((response) => { + if (response.ok) { + return response.json() + } + throw new Error('Error fetching version information'); + }) + .then((data) => { + if (data.total_commits > 0) { + this.systemDataList.update_text = "New version available! Show changes!" + this.systemDataList.update_status = "text-bg-danger"; + this.systemDataList.update_url = data.html_url; + } else { + this.systemDataList.update_text = "Up to date!" + this.systemDataList.update_status = "text-bg-success"; + } + }) + .catch((error: Error) => { + this.systemDataList.update_text = error.message; + this.systemDataList.update_status = "text-bg-secondary"; + }); + } }, }); diff --git a/webapp/src/components/partials/DevInfo.vue b/webapp/src/components/partials/DevInfo.vue index e8e35a9e..598c9c2b 100644 --- a/webapp/src/components/partials/DevInfo.vue +++ b/webapp/src/components/partials/DevInfo.vue @@ -1,5 +1,10 @@