diff --git a/src/CHANGES.md b/src/CHANGES.md index a2be17508..ca1f53325 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,12 @@ # Development Changes +## 0.8.43 - 2024-01-04 +* fix display of sunrise in `/system` #1308 +* fix overflow of `getLossRate` calculation #1318 +* improved MqTT by marking sent data and improved `last_success` resends #1319 +* added timestamp for `max ac power` as tooltip #1324 #1123 #1199 +* repaired Power-limit acknowledge #1322 + ## 0.8.42 - 2024-01-02 * add LED to display whether it's night time or not. Can be reused as output to control battery system #1308 * merge PR: beautifiying typography, added spaces between value and unit for `/visualization` #1314 diff --git a/src/app.cpp b/src/app.cpp index 6bce1709e..6a93f96c4 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -52,6 +52,7 @@ void app::setup() { mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->inst.gapMs); mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); + mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); }); mSys.setup(&mTimestamp, &mConfig->inst); for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { initInverter(i); diff --git a/src/app.h b/src/app.h index a24cccb38..8d6fafbe0 100644 --- a/src/app.h +++ b/src/app.h @@ -180,10 +180,6 @@ class app : public IApp, public ah::Scheduler { once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf"); } - void setMqttPowerLimitAck(Inverter<> *iv) { - mMqtt.setPowerLimitAck(iv); - } - bool getMqttIsConnected() { return mMqtt.isConnected(); } diff --git a/src/appInterface.h b/src/appInterface.h index 34dc5ddc4..9d6337653 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -52,7 +52,6 @@ class IApp { virtual bool getRebootRequestState() = 0; virtual bool getSettingsValid() = 0; virtual void setMqttDiscoveryFlag() = 0; - virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0; virtual bool getMqttIsConnected() = 0; virtual bool getNrfEnabled() = 0; diff --git a/src/defines.h b/src/defines.h index 4b3482a2e..21564fdb6 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 42 +#define VERSION_PATCH 43 //------------------------------------- typedef struct { diff --git a/src/hm/Communication.h b/src/hm/Communication.h index e31984b68..99c8f3820 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -19,6 +19,7 @@ #define MAX_BUFFER 250 typedef std::function *)> payloadListenerType; +typedef std::function *)> powerLimitAckListenerType; typedef std::function *)> alarmListenerType; class Communication : public CommQueue<> { @@ -40,6 +41,10 @@ class Communication : public CommQueue<> { mCbPayload = cb; } + void addPowerLimitAckListener(powerLimitAckListenerType cb) { + mCbPwrAck = cb; + } + void addAlarmListener(alarmListenerType cb) { mCbAlarm = cb; } @@ -401,6 +406,7 @@ class Communication : public CommQueue<> { DBGPRINT(F(" with PowerLimitControl ")); DBGPRINTLN(String(q->iv->powerLimit[1])); q->iv->actPowerLimit = 0xffff; // unknown, readback current value + (mCbPwrAck)(q->iv); return accepted; } @@ -921,6 +927,7 @@ class Communication : public CommQueue<> { uint8_t mMaxFrameId; uint8_t mPayload[MAX_BUFFER]; payloadListenerType mCbPayload = NULL; + powerLimitAckListenerType mCbPwrAck = NULL; alarmListenerType mCbAlarm = NULL; Heuristic mHeu; uint32_t mLastEmptyQueueMillis = 0; diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 57de135f1..1d38c23f4 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -64,13 +64,28 @@ struct calcFunc_t { func_t* func; // function pointer }; +enum class MqttSentStatus : uint8_t { + NEW_DATA, + LAST_SUCCESS_SENT, + DATA_SENT +}; + +enum class InverterStatus : uint8_t { + OFF, + STARTING, + PRODUCING, + WAS_PRODUCING, + WAS_ON +}; + template struct record_t { - byteAssign_t* assign; // assignment of bytes in payload - uint8_t length; // length of the assignment list - T *record; // data pointer - uint32_t ts; // timestamp of last received payload - uint8_t pyldLen; // expected payload length for plausibility check + byteAssign_t* assign; // assignment of bytes in payload + uint8_t length; // length of the assignment list + T *record; // data pointer + uint32_t ts; // timestamp of last received payload + uint8_t pyldLen; // expected payload length for plausibility check + MqttSentStatus mqttSentStatus; // indicates the current MqTT sent status }; struct alarm_t { @@ -94,14 +109,6 @@ const calcFunc_t calcFunctions[] = { { CALC_MPDC_CH, &calcMaxPowerDc } }; -enum class InverterStatus : uint8_t { - OFF, - STARTING, - PRODUCING, - WAS_PRODUCING, - WAS_ON -}; - template class Inverter { public: @@ -124,26 +131,28 @@ class Inverter { bool isConnected; // shows if inverter was successfully identified (fw version and hardware info) InverterStatus status; // indicates the current inverter status std::array lastAlarm; // holds last 10 alarms - uint8_t alarmNxtWrPos; // indicates the position in array (rolling buffer) + int8_t rssi; // RSSI uint16_t alarmCnt; // counts the total number of occurred alarms uint16_t alarmLastId; // lastId which was received - int8_t rssi; // RSSI + uint8_t mCmd; // holds the command to send + bool mGotFragment; // shows if inverter has sent at least one fragment uint8_t miMultiParts; // helper info for MI multiframe msgs uint8_t outstandingFrames; // helper info to count difference between expected and received frames - bool mGotFragment; // shows if inverter has sent at least one fragment uint8_t curFrmCnt; // count received frames in current loop bool mGotLastMsg; // shows if inverter has already finished transmission cycle - uint8_t mCmd; // holds the command to send bool mIsSingleframeReq; // indicates this is a missing single frame request Radio *radio; // pointer to associated radio class statistics_t radioStatistics; // information about transmitted, failed, ... packets HeuristicInv heuristics; // heuristic information / logic uint8_t curCmtFreq; // current used CMT frequency, used to check if freq. was changed during runtime bool commEnabled; // 'pause night communication' sets this field to false + uint32_t tsMaxAcPower; // holds the timestamp when the MaxAC power was seen static uint32_t *timestamp; // system timestamp static cfgInst_t *generalConfig; // general inverter configuration from setup + public: + Inverter() { ivGen = IV_HM; powerLimit[0] = 0xffff; // 6553.5 W Limit -> unlimited @@ -155,7 +164,6 @@ class Inverter { alarmMesIndex = 0; isConnected = false; status = InverterStatus::OFF; - alarmNxtWrPos = 0; alarmCnt = 0; alarmLastId = 0; rssi = -127; @@ -165,6 +173,7 @@ class Inverter { mIsSingleframeReq = false; radio = NULL; commEnabled = true; + tsMaxAcPower = 0; memset(&radioStatistics, 0, sizeof(statistics_t)); memset(heuristics.txRfQuality, -6, 5); @@ -310,11 +319,11 @@ class Inverter { rec->record[pos] = (REC_TYP)(val); } } + rec->mqttSentStatus = MqttSentStatus::NEW_DATA; } if(rec == &recordMeas) { DPRINTLN(DBG_VERBOSE, "add real time"); - // get last alarm message index and save it in the inverter object if (getPosByChFld(0, FLD_EVT, rec) == pos) { if (alarmMesIndex < rec->record[pos]) { @@ -498,6 +507,7 @@ class Inverter { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:initAssignment")); rec->ts = 0; rec->length = 0; + rec->mqttSentStatus = MqttSentStatus::DATA_SENT; // nothing new to transmit switch (cmd) { case RealTimeRunData_Debug: if (INV_TYPE_1CH == type) { @@ -582,7 +592,7 @@ class Inverter { void resetAlarms() { lastAlarm.fill({0, 0, 0}); - alarmNxtWrPos = 0; + mAlarmNxtWrPos = 0; alarmCnt = 0; alarmLastId = 0; @@ -596,10 +606,18 @@ class Inverter { uint16_t txCnt = (pyld[2] << 8) + pyld[3]; if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past - radioStatistics.ivLoss = mDtuTxCnt - (rxCnt - mIvRxCnt); radioStatistics.ivSent = mDtuTxCnt; - radioStatistics.dtuLoss = txCnt - mIvTxCnt - mDtuRxCnt; - radioStatistics.dtuSent = txCnt - mIvTxCnt; + if (rxCnt < mIvRxCnt) // overflow + radioStatistics.ivLoss = radioStatistics.ivSent - (rxCnt + ((uint16_t)65535 - mIvRxCnt) + 1); + else + radioStatistics.ivLoss = radioStatistics.ivSent - (rxCnt - mIvRxCnt); + + if (txCnt < mIvTxCnt) // overflow + radioStatistics.dtuSent = txCnt + ((uint16_t)65535 - mIvTxCnt) + 1; + else + radioStatistics.dtuSent = txCnt - mIvTxCnt; + + radioStatistics.dtuLoss = radioStatistics.dtuSent - mDtuRxCnt; DPRINT_IVID(DBG_INFO, id); DBGPRINT(F("Inv loss: ")); @@ -790,9 +808,9 @@ class Inverter { private: inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) { - lastAlarm[alarmNxtWrPos] = alarm_t(code, start, end); - if(++alarmNxtWrPos >= 10) // rolling buffer - alarmNxtWrPos = 0; + lastAlarm[mAlarmNxtWrPos] = alarm_t(code, start, end); + if(++mAlarmNxtWrPos >= 10) // rolling buffer + mAlarmNxtWrPos = 0; } void toRadioId(void) { @@ -813,6 +831,7 @@ class Inverter { uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug uint16_t mIvRxCnt = 0; uint16_t mIvTxCnt = 0; + uint8_t mAlarmNxtWrPos = 0; // indicates the position in array (rolling buffer) public: uint16_t mDtuRxCnt = 0; @@ -948,8 +967,10 @@ static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) { dcMaxPower = iv->getValue(i, rec); } } - if(dcPower > dcMaxPower) + if(dcPower > dcMaxPower) { + iv->tsMaxAcPower = *iv->timestamp; return dcPower; + } } return dcMaxPower; } diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index c645c70b7..61344edec 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -28,7 +28,6 @@ class PubMqttIvData { mState = IDLE; mZeroValues = false; - memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * sizeof(uint32_t)); mRTRDataHasBeenSent = false; mTable[IDLE] = &PubMqttIvData::stateIdle; @@ -102,7 +101,7 @@ class PubMqttIvData { mPos = 0; if(found) { record_t<> *rec = mIv->getRecordStruct(mCmd); - if((RealTimeRunData_Debug == mCmd) && mIv->getLastTs(rec) != 0 ) { //workaround for startup. Suspect, mCmd might cause to much messages.... + if(MqttSentStatus::NEW_DATA == rec->mqttSentStatus) { snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", mIv->config->name); snprintf(mVal, 40, "%d", mIv->getLastTs(rec)); mPublish(mSubTopic, mVal, true, QOS_0); @@ -112,13 +111,14 @@ class PubMqttIvData { snprintf(mVal, 40, "%d", mIv->rssi); mPublish(mSubTopic, mVal, false, QOS_0); } + rec->mqttSentStatus = MqttSentStatus::LAST_SUCCESS_SENT; } mIv->isProducing(); // recalculate status mState = SEND_DATA; - } else if(mSendTotals && mTotalFound) + } else if(mSendTotals && mTotalFound) { mState = SEND_TOTALS; - else { + } else { mSendList->pop(); mZeroValues = false; mState = START; @@ -132,12 +132,8 @@ class PubMqttIvData { DPRINT(DBG_WARN, "unknown record to publish!"); return; } - uint32_t lastTs = mIv->getLastTs(rec); - bool pubData = (lastTs > 0); - if (mCmd == RealTimeRunData_Debug) - pubData &= (lastTs != mIvLastRTRpub[mIv->id]); - if (pubData) { + if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) { if(mPos < rec->length) { bool retained = false; if (mCmd == RealTimeRunData_Debug) { @@ -172,21 +168,16 @@ class PubMqttIvData { } else mAllTotalFound = false; } - } else - mIvLastRTRpub[mIv->id] = lastTs; - - uint8_t qos = QOS_0; - if(FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) - qos = QOS_2; - - if((mIvSend == mIv) || (NULL == mIvSend)) { // send only updated values, or all if the inverter is NULL - snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]); - snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec))); - mPublish(mSubTopic, mVal, retained, qos); } + + uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0; + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]); + snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec))); + mPublish(mSubTopic, mVal, retained, qos); mPos++; } else { sendRadioStat(rec->length); + rec->mqttSentStatus = MqttSentStatus::DATA_SENT; mState = FIND_NXT_IV; } } else @@ -263,7 +254,6 @@ class PubMqttIvData { Inverter<> *mIv, *mIvSend; uint8_t mPos; - uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS]; bool mRTRDataHasBeenSent; char mSubTopic[32 + MAX_NAME_LENGTH + 1]; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 03275eed8..2b341441e 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -485,6 +485,7 @@ class RestApi { obj[F("status")] = (uint8_t)iv->getStatus(); obj[F("alarm_cnt")] = iv->alarmCnt; obj[F("rssi")] = iv->rssi; + obj[F("ts_max_ac_pwr")] = iv->tsMaxAcPower; JsonArray ch = obj.createNestedArray("ch"); diff --git a/src/web/html/index.html b/src/web/html/index.html index 3ac72e89d..cfdaeee61 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -99,17 +99,17 @@

Support this project:

} if(obj.disNightComm) { - if(((obj.ts_sunrise - obj.ts_offsSr) < obj.ts_now) + if(((obj.ts_sunrise + obj.ts_offsSr) < obj.ts_now) && ((obj.ts_sunset + obj.ts_offsSs) > obj.ts_now)) { commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); } else { commInfo = "Night time, inverter polling disabled, "; - if(obj.ts_now > (obj.ts_sunrise - obj.ts_offsSr)) { + if(obj.ts_now > (obj.ts_sunrise + obj.ts_offsSr)) { commInfo += "paused at " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')); } else { - commInfo += "will start polling at " + (new Date((obj.ts_sunrise - obj.ts_offsSr) * 1000).toLocaleString('de-DE')); + commInfo += "will start polling at " + (new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')); } } } diff --git a/src/web/html/setup.html b/src/web/html/setup.html index a103cf15c..782c23beb 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -765,7 +765,7 @@ ml("div", {class: "col-2"}, cbDisNightCom) ]), ml("div", {class: "row mb-3"}, [ - ml("div", {class: "col-10"}, "Include inverter to sum of total (should be checked by default)"), + ml("div", {class: "col-10"}, "Include inverter to sum of total (should be checked by default, MqTT only)"), ml("div", {class: "col-2"}, cbAddTotal) ]) ]), diff --git a/src/web/html/style.css b/src/web/html/style.css index b31ae7c93..e7de68f43 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -650,39 +650,24 @@ div.hr { } -.css-tooltip{ +.tooltip{ position: relative; } -.css-tooltip:hover:after{ - content:attr(data-tooltip); - background:#000; - padding:5px; - border-radius:3px; +.tooltip:hover:after { + content: attr(data); + background: var(--nav-active); + padding: 5px; + border-radius: 3px; display: inline-block; position: absolute; transform: translate(-50%,-100%); margin:0 auto; - color:#FFF; - min-width:100px; - min-width:150px; - top:-5px; + color: var(--fg2); + min-width: 100px; + top: -5px; left: 50%; text-align:center; -} -.css-tooltip:hover:before { - top:-5px; - left: 50%; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - border-color: rgba(0, 0, 0, 0); - border-top-color: #000; - border-width: 5px; - margin-left: -5px; - transform: translate(0,0px); + font-size: 1rem; } #modal { diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index ec6596bbb..3ae8ff0b1 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -45,13 +45,14 @@ ]); } - function numMid(val, unit, des) { + function numMid(val, unit, des, opt={class: "fs-6"}) { return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [ ml("div", {class: "row"}, ml("div", {class: "col"}, [ - ml("span", {class: "fs-6"}, String(Math.round(val * 100) / 100)), + ml("span", opt, String(Math.round(val * 100) / 100)), ml("span", {class: "fs-8 mx-1"}, unit) - ])), + ]) + ), ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-9"}, des) @@ -108,6 +109,8 @@ if(0 != obj.max_pwr) pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + " W"; } + + var maxAcPwr = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000)); return ml("div", {class: "row mt-2"}, ml("div", {class: "col"}, [ ml("div", {class: "p-2 " + clh}, @@ -133,7 +136,7 @@ ]), ml("div", {class: "hr"}), ml("div", {class: "row mt-2"},[ - numMid(obj.ch[0][11], "W", "Max AC Power"), + 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][0], "V", "AC Voltage"), numMid(obj.ch[0][1], "A", "AC Current"),