diff --git a/doc/prometheus_ep_description.md b/doc/prometheus_ep_description.md index 651cd937..6b4ce56c 100644 --- a/doc/prometheus_ep_description.md +++ b/doc/prometheus_ep_description.md @@ -25,7 +25,7 @@ Prometheus metrics provided at `/metrics`. | `ahoy_solar_inverter_enabled` | Gauge | Is the inverter enabled? | inverter | | `ahoy_solar_inverter_is_available` | Gauge | is the inverter available? | inverter | | `ahoy_solar_inverter_is_producing` | Gauge | Is the inverter producing? | inverter | -| `ahoy_solar_inverter_power_limit_read` | Gauge | Power Limit read from inverter | inverter | +| `ahoy_solar_inverter_power_limit_read` | Gauge | Power Limit read from inverter. Defaults to 65535 | inverter | | `ahoy_solar_inverter_power_limit_ack` | Gauge | Power Limit acknowledged by inverter | inverter | | `ahoy_solar_inverter_max_power` | Gauge | Max Power of inverter | inverter | | `ahoy_solar_inverter_radio_rx_success` | Counter | NRF24 statistic of inverter | inverter | @@ -34,6 +34,10 @@ Prometheus metrics provided at `/metrics`. | `ahoy_solar_inverter_radio_frame_cnt` | Counter | NRF24 statistic of inverter | inverter | | `ahoy_solar_inverter_radio_tx_cnt` | Counter | NRF24 statistic of inverter | inverter | | `ahoy_solar_inverter_radio_retransmits` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_iv_rx_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_iv_tx_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_dtu_rx_cnt` | Counter | NRF24 statistic of inverter | inverter | +| `ahoy_solar_inverter_radio_dtu_rx_cnt` | Counter | NRF24 statistic of inverter | inverter | | `ahoy_solar_U_AC_volt` | Gauge | AC voltage of inverter [V] | inverter | | `ahoy_solar_I_AC_ampere` | Gauge | AC current of inverter [A] | inverter | | `ahoy_solar_P_AC_watt` | Gauge | AC power of inverter [W] | inverter | diff --git a/src/CHANGES.md b/src/CHANGES.md index 43e58613..9dc10a72 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,25 @@ # Development Changes +## 0.8.44 - 2024-01-05 +* fix MqTT transmission of data #1326 +* live data is read much earlier / faster and more often + +## 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 +* fix `max_power` in `/visualization` was set to `0` after sunset + +## 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 +* merge PR: Prometheus add `getLossRate` and bugfixing #1315 +* add loss rate to `/visualization` in the statistics window +* corrected `getLossRate` infos for MqTT and prometheus +* added information about working IRQ for NRF24 and CMT2300A to `/system` + ## 0.8.41 - 2024-01-02 * fix display timeout (OLED) to 60s * change offs to signed value diff --git a/src/app.cpp b/src/app.cpp index b70f894c..07571a30 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); @@ -472,14 +473,11 @@ void app::mqttSubRxCb(JsonObject obj) { //----------------------------------------------------------------------------- void app::setupLed(void) { uint8_t led_off = (mConfig->led.high_active) ? 0 : 255; - - if (mConfig->led.led0 != DEF_PIN_OFF) { - pinMode(mConfig->led.led0, OUTPUT); - analogWrite(mConfig->led.led0, led_off); - } - if (mConfig->led.led1 != DEF_PIN_OFF) { - pinMode(mConfig->led.led1, OUTPUT); - analogWrite(mConfig->led.led1, led_off); + for(uint8_t i = 0; i < 3; i ++) { + if (mConfig->led.led[i] != DEF_PIN_OFF) { + pinMode(mConfig->led.led[i], OUTPUT); + analogWrite(mConfig->led.led[i], led_off); + } } } @@ -488,29 +486,36 @@ void app::updateLed(void) { uint8_t led_off = (mConfig->led.high_active) ? 0 : 255; uint8_t led_on = (mConfig->led.high_active) ? (mConfig->led.luminance) : (255-mConfig->led.luminance); - if (mConfig->led.led0 != DEF_PIN_OFF) { + if (mConfig->led.led[0] != DEF_PIN_OFF) { Inverter<> *iv; for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); if (NULL != iv) { if (iv->isProducing()) { // turn on when at least one inverter is producing - analogWrite(mConfig->led.led0, led_on); + analogWrite(mConfig->led.led[0], led_on); break; } else if(iv->config->enabled) - analogWrite(mConfig->led.led0, led_off); + analogWrite(mConfig->led.led[0], led_off); } } } - if (mConfig->led.led1 != DEF_PIN_OFF) { + if (mConfig->led.led[1] != DEF_PIN_OFF) { if (getMqttIsConnected()) { - analogWrite(mConfig->led.led1, led_on); + analogWrite(mConfig->led.led[1], led_on); } else { - analogWrite(mConfig->led.led1, led_off); + analogWrite(mConfig->led.led[1], led_off); } } + + if (mConfig->led.led[2] != DEF_PIN_OFF) { + if((mTimestamp > (mSunset + mConfig->sun.offsetSecEvening)) || (mTimestamp < (mSunrise + mConfig->sun.offsetSecMorning))) + analogWrite(mConfig->led.led[2], led_on); + else + analogWrite(mConfig->led.led[2], led_off); + } } //----------------------------------------------------------------------------- #if defined(ESP32) diff --git a/src/app.h b/src/app.h index 15265298..eae9d74f 100644 --- a/src/app.h +++ b/src/app.h @@ -185,10 +185,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 34dc5ddc..9d633765 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/config/config.h b/src/config/config.h index ba477701..e9588fb6 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -142,6 +142,9 @@ #ifndef DEF_LED1 #define DEF_LED1 DEF_PIN_OFF #endif +#ifndef DEF_LED2 + #define DEF_LED2 DEF_PIN_OFF +#endif #ifdef LED_ACTIVE_HIGH #define LED_HIGH_ACTIVE true #else diff --git a/src/config/settings.h b/src/config/settings.h index e2c6aa26..833154ef 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -118,8 +118,7 @@ typedef struct { } cfgSerial_t; typedef struct { - uint8_t led0; // first LED pin - uint8_t led1; // second LED pin + uint8_t led[3]; // LED pins bool high_active; // determines if LEDs are high or low active uint8_t luminance; // luminance of LED } cfgLed_t; @@ -492,8 +491,9 @@ class settings { mCfg.inst.iv[i].add2Total = true; } - mCfg.led.led0 = DEF_LED0; - mCfg.led.led1 = DEF_LED1; + mCfg.led.led[0] = DEF_LED0; + mCfg.led.led[1] = DEF_LED1; + mCfg.led.led[2] = DEF_LED2; mCfg.led.high_active = LED_HIGH_ACTIVE; mCfg.led.luminance = 255; @@ -751,13 +751,15 @@ class settings { void jsonLed(JsonObject obj, bool set = false) { if(set) { - obj[F("0")] = mCfg.led.led0; - obj[F("1")] = mCfg.led.led1; + obj[F("0")] = mCfg.led.led[0]; + obj[F("1")] = mCfg.led.led[1]; + obj[F("2")] = mCfg.led.led[2]; obj[F("act_high")] = mCfg.led.high_active; obj[F("lum")] = mCfg.led.luminance; } else { - getVal(obj, F("0"), &mCfg.led.led0); - getVal(obj, F("1"), &mCfg.led.led1); + getVal(obj, F("0"), &mCfg.led.led[0]); + getVal(obj, F("1"), &mCfg.led.led[1]); + getVal(obj, F("2"), &mCfg.led.led[2]); getVal(obj, F("act_high"), &mCfg.led.high_active); getVal(obj, F("lum"), &mCfg.led.luminance); } diff --git a/src/defines.h b/src/defines.h index bd383927..6abcbd1c 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 41 +#define VERSION_PATCH 44 //------------------------------------- typedef struct { @@ -106,10 +106,10 @@ typedef struct { uint32_t frmCnt; uint32_t txCnt; uint32_t retransmits; - uint16_t ivRxCnt; // last iv rx frames (from GetLossRate) - uint16_t ivTxCnt; // last iv tx frames (from GetLossRate) - uint16_t dtuRxCnt; // current DTU rx frames (since last GetLossRate) - uint16_t dtuTxCnt; // current DTU tx frames (since last GetLossRate) + uint16_t ivLoss; // lost frames (from GetLossRate) + uint16_t ivSent; // sent frames (from GetLossRate) + uint16_t dtuLoss; // current DTU lost frames (since last GetLossRate) + uint16_t dtuSent; // current DTU sent frames (since last GetLossRate) } statistics_t; #endif /*__DEFINES_H__*/ diff --git a/src/hm/Communication.h b/src/hm/Communication.h index d44e860f..219cb92e 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; } @@ -151,7 +156,7 @@ class Communication : public CommQueue<> { if(validateIvSerial(&p->packet[1], q->iv)) { q->iv->radioStatistics.frmCnt++; - q->iv->radioStatistics.dtuRxCnt++; + q->iv->mDtuRxCnt++; if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command if(parseFrame(p)) @@ -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; } @@ -492,6 +498,7 @@ class Communication : public CommQueue<> { for (uint8_t i = 0; i < rec->length; i++) { q->iv->addValue(i, mPayload, rec); } + rec->mqttSentStatus = MqttSentStatus::NEW_DATA; q->iv->rssi = rssi; q->iv->doCalculations(); @@ -921,6 +928,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 25f575e5..a5834d9a 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); @@ -177,28 +186,33 @@ class Inverter { if(mDevControlRequest) { cb(devControlCmd, true); mDevControlRequest = false; - } else if (IV_MI != ivGen) { + } else if (IV_MI != ivGen) { // HM / HMS / HMT mGetLossInterval++; - if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) - cb(AlarmData, false); // get last alarms - else if(0 == getFwVersion()) - cb(InverterDevInform_All, false); // get firmware version - else if(0 == getHwVersion()) - cb(InverterDevInform_Simple, false); // get hardware version - else if(actPowerLimit == 0xffff) - cb(SystemConfigPara, false); // power limit info - else if(InitDataState != devControlCmd) { - cb(devControlCmd, false); // custom command which was received by API - devControlCmd = InitDataState; - mGetLossInterval = 1; - } else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile - cb(GridOnProFilePara, false); - } else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate - mGetLossInterval = 1; - cb(GetLossRate, false); - } else + if(mNextLive) cb(RealTimeRunData_Debug, false); // get live data - } else { + else { + mNextLive = true; + if(actPowerLimit == 0xffff) + cb(SystemConfigPara, false); // power limit info + else if(InitDataState != devControlCmd) { + cb(devControlCmd, false); // custom command which was received by API + devControlCmd = InitDataState; + mGetLossInterval = 1; + } else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) + cb(AlarmData, false); // get last alarms + else if(0 == getFwVersion()) + cb(InverterDevInform_All, false); // get firmware version + else if(0 == getHwVersion()) + cb(InverterDevInform_Simple, false); // get hardware version + else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile + cb(GridOnProFilePara, false); + } else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate + mGetLossInterval = 1; + cb(GetLossRate, false); + } else + cb(RealTimeRunData_Debug, false); // get live data + } + } else { // MI if(0 == getFwVersion()) cb(0x0f, false); // get firmware version; for MI, this makes part of polling the device software and hardware version number else { @@ -313,8 +327,8 @@ class Inverter { } if(rec == &recordMeas) { + mNextLive = false; // live data received 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 +512,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 +597,7 @@ class Inverter { void resetAlarms() { lastAlarm.fill({0, 0, 0}); - alarmNxtWrPos = 0; + mAlarmNxtWrPos = 0; alarmCnt = 0; alarmLastId = 0; @@ -595,22 +610,35 @@ class Inverter { uint16_t rxCnt = (pyld[0] << 8) + pyld[1]; uint16_t txCnt = (pyld[2] << 8) + pyld[3]; - if (radioStatistics.ivRxCnt || radioStatistics.ivTxCnt) { // there was successful GetLossRate in the past + if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past + radioStatistics.ivSent = mDtuTxCnt; + 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: ")); - DBGPRINT(String (radioStatistics.dtuTxCnt - (rxCnt - radioStatistics.ivRxCnt))); + DBGPRINT(String(radioStatistics.ivLoss)); DBGPRINT(F(" of ")); - DBGPRINT(String (radioStatistics.dtuTxCnt)); + DBGPRINT(String(radioStatistics.ivSent)); DBGPRINT(F(", DTU loss: ")); - DBGPRINT(String (txCnt - radioStatistics.ivTxCnt - radioStatistics.dtuRxCnt)); + DBGPRINT(String(radioStatistics.dtuLoss)); DBGPRINT(F(" of ")); - DBGPRINTLN(String (txCnt - radioStatistics.ivTxCnt)); + DBGPRINTLN(String(radioStatistics.dtuSent)); } - radioStatistics.ivRxCnt = rxCnt; - radioStatistics.ivTxCnt = txCnt; - radioStatistics.dtuRxCnt = 0; // start new interval - radioStatistics.dtuTxCnt = 0; // start new interval + mIvRxCnt = rxCnt; + mIvTxCnt = txCnt; + mDtuRxCnt = 0; // start new interval + mDtuTxCnt = 0; // start new interval return true; } @@ -785,9 +813,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) { @@ -806,6 +834,14 @@ class Inverter { uint8_t mGridLen = 0; uint8_t mGridProfile[MAX_GRID_LENGTH]; 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) + bool mNextLive = true; // first read live data after booting up then version etc. + + public: + uint16_t mDtuRxCnt = 0; + uint16_t mDtuTxCnt = 0; }; template @@ -937,8 +973,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/hm/hmRadio.h b/src/hm/hmRadio.h index c7b9581c..6539dd21 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -339,7 +339,7 @@ class HmRadio : public Radio { mMillis = millis(); mLastIv = iv; - iv->radioStatistics.dtuTxCnt++; + iv->mDtuTxCnt++; } uint64_t getIvId(Inverter<> *iv) { diff --git a/src/hm/radio.h b/src/hm/radio.h index 2fe4f640..1ef32e05 100644 --- a/src/hm/radio.h +++ b/src/hm/radio.h @@ -14,6 +14,8 @@ #include "../utils/dbg.h" #include "../utils/crc.h" +enum { IRQ_UNKNOWN = 0, IRQ_OK, IRQ_ERROR }; + // forward declaration of class template class Inverter; @@ -30,6 +32,7 @@ class Radio { void handleIntr(void) { mIrqRcvd = true; + mIrqOk = IRQ_OK; } void sendCmdPacket(Inverter<> *iv, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) { @@ -65,6 +68,7 @@ class Radio { public: std::queue mBufCtrl; + uint8_t mIrqOk = IRQ_UNKNOWN; protected: virtual void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) = 0; @@ -77,6 +81,8 @@ class Radio { CP_U32_LittleEndian(&mTxBuf[5], mDtuSn); mTxBuf[9] = pid; memset(&mTxBuf[10], 0x00, (MAX_RF_PAYLOAD_SIZE-10)); + if(IRQ_UNKNOWN == mIrqOk) + mIrqOk = IRQ_ERROR; } void updateCrcs(uint8_t *len, bool appendCrc16=true) { diff --git a/src/hms/hmsRadio.h b/src/hms/hmsRadio.h index 3b3893ac..6b502816 100644 --- a/src/hms/hmsRadio.h +++ b/src/hms/hmsRadio.h @@ -112,7 +112,7 @@ class CmtRadio : public Radio { if(CMT_ERR_RX_IN_FIFO == status) mIrqRcvd = true; } - iv->radioStatistics.dtuTxCnt++; + iv->mDtuTxCnt++; } uint64_t getIvId(Inverter<> *iv) { diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index 3a38a5ed..61344ede 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -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 //----------------------------------------------------------------------------- @@ -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 @@ -201,10 +192,10 @@ class PubMqttIvData { mIv->radioStatistics.rxFail, mIv->radioStatistics.rxFailNoAnser, mIv->radioStatistics.retransmits, - mIv->radioStatistics.ivRxCnt, - mIv->radioStatistics.ivTxCnt, - mIv->radioStatistics.dtuRxCnt, - mIv->radioStatistics.dtuTxCnt); + mIv->radioStatistics.ivLoss, + mIv->radioStatistics.ivSent, + mIv->radioStatistics.dtuLoss, + mIv->radioStatistics.dtuSent); mPublish(mSubTopic, mVal, false, QOS_0); } @@ -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 283674c2..bcd39381 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -405,6 +405,10 @@ class RestApi { obj[F("frame_cnt")] = iv->radioStatistics.frmCnt; obj[F("tx_cnt")] = iv->radioStatistics.txCnt; obj[F("retransmits")] = iv->radioStatistics.retransmits; + obj[F("ivLoss")] = iv->radioStatistics.ivLoss; + obj[F("ivSent")] = iv->radioStatistics.ivSent; + obj[F("dtuLoss")] = iv->radioStatistics.dtuLoss; + obj[F("dtuSent")] = iv->radioStatistics.dtuSent; } void getIvPowerLimitAck(JsonObject obj, uint8_t id) { @@ -481,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"); @@ -627,8 +632,9 @@ class RestApi { obj[F("sclk")] = mConfig->nrf.pinSclk; obj[F("mosi")] = mConfig->nrf.pinMosi; obj[F("miso")] = mConfig->nrf.pinMiso; - obj[F("led0")] = mConfig->led.led0; - obj[F("led1")] = mConfig->led.led1; + obj[F("led0")] = mConfig->led.led[0]; + obj[F("led1")] = mConfig->led.led[1]; + obj[F("led2")] = mConfig->led.led[2]; obj[F("led_high_active")] = mConfig->led.high_active; obj[F("led_lum")] = mConfig->led.luminance; } @@ -648,6 +654,7 @@ class RestApi { if(mConfig->cmt.enabled) { obj[F("isconnected")] = mRadioCmt->isChipConnected(); obj[F("sn")] = String(mRadioCmt->getDTUSn(), HEX); + obj[F("irqOk")] = mRadioCmt->mIrqOk; } } #endif @@ -658,6 +665,7 @@ class RestApi { obj[F("isconnected")] = mRadioNrf->isChipConnected(); obj[F("dataRate")] = mRadioNrf->getDataRate(); obj[F("sn")] = String(mRadioNrf->getDTUSn(), HEX); + obj[F("irqOk")] = mRadioNrf->mIrqOk; } } diff --git a/src/web/html/index.html b/src/web/html/index.html index 3ac72e89..cfdaeee6 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -99,17 +99,17 @@ } 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 89d58e5c..c5e2e0fc 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -818,7 +818,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) ]) ]), @@ -962,7 +962,7 @@ if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins; else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; /*ENDIF_ESP32*/ - pins = [['led0', 'pinLed0', 'At least one inverter is producing'], ['led1', 'pinLed1', 'MqTT connected']]; + pins = [['led0', 'pinLed0', 'At least one inverter is producing'], ['led1', 'pinLed1', 'MqTT connected'], ['led2', 'pinLed2', 'Night time']]; for(p of pins) { e.append( ml("div", {class: "row mb-3"}, [ diff --git a/src/web/html/style.css b/src/web/html/style.css index e0eae098..09a72522 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -654,39 +654,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/system.html b/src/web/html/system.html index 035f30ff..f400db41 100644 --- a/src/web/html/system.html +++ b/src/web/html/system.html @@ -44,12 +44,21 @@ return ml("div", {class: "head p-2 mt-3"}, ml("div", {class: "row"}, ml("div", {class: "col a-c"}, text))) } + function irqBadge(state) { + switch(state) { + case 0: return badge(false, "unknown", "warning"); break; + case 1: return badge(true, "true"); break; + default: return badge(false, "false"); break; + } + } + function parseRadio(obj) { const dr = ["1 M", "2 M", "250 k"] if(obj.radioNrf.en) { lines = [ tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "not ") + "connected")), + tr("Interrupt Pin working", irqBadge(obj.radioNrf.irqOk)), tr("NRF24 Data Rate", dr[obj.radioNrf.dataRate] + "bps"), tr("DTU Radio ID", obj.radioNrf.sn) ]; @@ -67,6 +76,7 @@ if(obj.radioCmt.en) { cmt = [ tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "not ") + "connected")), + tr("Interrupt Pin working", irqBadge(obj.radioCmt.irqOk)), tr("DTU Radio ID", obj.radioCmt.sn) ]; } else diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 179008c5..cbc69aa7 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -46,13 +46,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) @@ -92,10 +93,10 @@ function ivHead(obj) { if(0 != obj.status) { // only add totals if inverter is online total[0] += obj.ch[0][2]; // P_AC - total[3] += obj.ch[0][11]; // MAX P_AC total[4] += obj.ch[0][8]; // P_DC total[5] += obj.ch[0][10]; // Q_AC } + total[3] += obj.ch[0][11]; // MAX P_AC total[1] += obj.ch[0][7]; // YieldDay total[2] += obj.ch[0][6]; // YieldTotal @@ -107,8 +108,10 @@ if(65535 != obj.power_limit_read) { pwrLimit = obj.power_limit_read + " %"; if(0 != obj.max_pwr) - pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "W"; + 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}, @@ -134,7 +137,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"), @@ -193,9 +196,9 @@ if(obj.rssi > -127) { if(obj.generation < 2) - ageInfo += " (RSSI: " + ((obj.rssi == -64) ? ">=" : "<") + " -64dBm)"; + ageInfo += " (RSSI: " + ((obj.rssi == -64) ? ">=" : "<") + " -64 dBm)"; else - ageInfo += " (RSSI: " + obj.rssi + "dBm)"; + ageInfo += " (RSSI: " + obj.rssi + " dBm)"; } return ml("div", {class: "mb-5"}, [ @@ -385,11 +388,13 @@ var html = ml("table", {class: "table"}, [ ml("tbody", {}, [ tr2(["TX count", obj.tx_cnt, ""]), - tr2(["RX success", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + "%"]), - tr2(["RX fail", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + "%"]), - tr2(["RX no answer", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + "%"]), + tr2(["RX success", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + " %"]), + tr2(["RX fail", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + " %"]), + tr2(["RX no answer", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + " %"]), tr2(["RX fragments", obj.frame_cnt, ""]), - tr2(["TX retransmits", obj.retransmits, ""]) + tr2(["TX retransmits", obj.retransmits, ""]), + tr2(["Inverter loss rate", "lost " + obj.ivLoss + " of " + obj.ivSent, String(Math.round(obj.ivLoss / obj.ivSent * 10000) / 100) + " %"]), + tr2(["DTU loss rate", "lost " + obj.dtuLoss + " of " + obj.dtuSent, String(Math.round(obj.dtuLoss / obj.dtuSent * 10000) / 100) + " %"]) ]) ]) modal("Radio statistics for inverter " + obj.name, ml("div", {}, html)) diff --git a/src/web/web.h b/src/web/web.h index d7f93dd8..1757f97a 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -39,7 +39,7 @@ #define WEB_SERIAL_BUF_SIZE 2048 -const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLedHighActive", "pinLedLum", "pinCmtSclk", "pinSdio", "pinCsb", "pinFcsb", "pinGpio3"}; +const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLed2", "pinLedHighActive", "pinLedLum", "pinCmtSclk", "pinSdio", "pinCsb", "pinFcsb", "pinGpio3"}; template class Web { @@ -506,7 +506,7 @@ class Web { // pinout uint8_t pin; - for (uint8_t i = 0; i < 15; i++) { + for (uint8_t i = 0; i < 16; i++) { pin = request->arg(String(pinArgNames[i])).toInt(); switch(i) { case 0: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_NRF_CS_PIN); break; @@ -515,15 +515,16 @@ class Web { case 3: mConfig->nrf.pinSclk = ((pin != 0xff) ? pin : DEF_NRF_SCLK_PIN); break; case 4: mConfig->nrf.pinMosi = ((pin != 0xff) ? pin : DEF_NRF_MOSI_PIN); break; case 5: mConfig->nrf.pinMiso = ((pin != 0xff) ? pin : DEF_NRF_MISO_PIN); break; - case 6: mConfig->led.led0 = pin; break; - case 7: mConfig->led.led1 = pin; break; - case 8: mConfig->led.high_active = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense - case 9: mConfig->led.luminance = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense - case 10: mConfig->cmt.pinSclk = pin; break; - case 11: mConfig->cmt.pinSdio = pin; break; - case 12: mConfig->cmt.pinCsb = pin; break; - case 13: mConfig->cmt.pinFcsb = pin; break; - case 14: mConfig->cmt.pinIrq = pin; break; + case 6: mConfig->led.led[0] = pin; break; + case 7: mConfig->led.led[1] = pin; break; + case 8: mConfig->led.led[2] = pin; break; + case 9: mConfig->led.high_active = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense + case 10: mConfig->led.luminance = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense + case 11: mConfig->cmt.pinSclk = pin; break; + case 12: mConfig->cmt.pinSdio = pin; break; + case 13: mConfig->cmt.pinCsb = pin; break; + case 14: mConfig->cmt.pinFcsb = pin; break; + case 15: mConfig->cmt.pinIrq = pin; break; } } @@ -661,13 +662,16 @@ class Web { // NOTE: Grouping for fields with channels and totals is currently not working // TODO: Handle grouping and sorting for independant from channel number // NOTE: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels) - const char * metricPrefix = "ahoy_solar_"; + const char * metricConstPrefix = "ahoy_solar_"; + const char * metricConstInverterFormat = " {inverter=\"%s\"} %d\n"; typedef enum { - metricsStateInverterInfo=0, metricsStateInverterEnabled=1, metricsStateInverterAvailable=2, metricsStateInverterProducing=3, - metricsStateInverterPowerLimitRead=4, metricsStateInverterPowerLimitAck=5, metricsStateInverterMaxPower=6, - metricsStateInverterRxSuccess=7, metricsStateInverterRxFail=8, metricsStateInverterRxFailAnswer=9, - metricsStateInverterFrameCnt=10, metricsStateInverterTxCnt=11, metricsStateInverterRetransmits=12, - metricStateRealtimeFieldId=metricsStateInverterRetransmits+1, // ensure that this state follows the last per_inverter state + metricsStateInverterInfo=0, metricsStateInverterEnabled=1, metricsStateInverterAvailable=2, + metricsStateInverterProducing=3, metricsStateInverterPowerLimitRead=4, metricsStateInverterPowerLimitAck=5, + metricsStateInverterMaxPower=6, metricsStateInverterRxSuccess=7, metricsStateInverterRxFail=8, + metricsStateInverterRxFailAnswer=9, metricsStateInverterFrameCnt=10, metricsStateInverterTxCnt=11, + metricsStateInverterRetransmits=12, metricsStateInverterIvRxCnt=13, metricsStateInverterIvTxCnt=14, + metricsStateInverterDtuRxCnt=15, metricsStateInverterDtuTxCnt=16, + metricStateRealtimeFieldId=metricsStateInverterDtuTxCnt+1, // ensure that this state follows the last per_inverter state metricStateRealtimeInverterId, metricsStateAlarmData, metricsStateStart, @@ -675,24 +679,29 @@ class Web { } MetricStep_t; MetricStep_t metricsStep; typedef struct { + const char *topic; const char *type; const char *format; const std::function *iv)> valueFunc; } InverterMetric_t; - InverterMetric_t inverterMetrics[13] = { - { "info", "info{name=\"%s\",serial=\"%12llx\"} 1\n", [](Inverter<> *iv)-> uint64_t {return iv->config->serial.u64;} }, - { "is_enabled", "is_enabled {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->config->enabled;} }, - { "is_available", "is_available {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->isAvailable();} }, - { "is_producing", "is_producing {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->isProducing();} }, - { "power_limit_read", "power_limit_read {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return (int64_t)ah::round3(iv->actPowerLimit);} }, - { "power_limit_ack", "power_limit_ack {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return (iv->powerLimitAck)?1:0;} }, - { "max_power", "max_power {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} }, - { "radio_rx_success", "radio_rx_success {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} }, - { "radio_rx_fail", "radio_rx_fail {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} }, - { "radio_rx_fail_answer", "radio_rx_fail_answer {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} }, - { "radio_frame_cnt", "radio_frame_cnt {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} }, - { "radio_tx_cnt", "radio_tx_cnt {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} }, - { "radio_retransmits", "radio_retransmits {inverter=\"%s\"} %d\n", [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} } + InverterMetric_t inverterMetrics[17] = { + { "info", "gauge", " {name=\"%s\",serial=\"%12llx\"} 1\n", [](Inverter<> *iv)-> uint64_t {return iv->config->serial.u64;} }, + { "is_enabled", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->config->enabled;} }, + { "is_available", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isAvailable();} }, + { "is_producing", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isProducing();} }, + { "power_limit_read", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return (int64_t)ah::round3(iv->actPowerLimit);} }, + { "power_limit_ack", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return (iv->powerLimitAck)?1:0;} }, + { "max_power", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} }, + { "radio_rx_success", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} }, + { "radio_rx_fail", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} }, + { "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} }, + { "radio_frame_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} }, + { "radio_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} }, + { "radio_retransmits", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} }, + { "radio_iv_loss_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.ivLoss;} }, + { "radio_iv_sent_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.ivSent;} }, + { "radio_dtu_loss_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuLoss;} }, + { "radio_dtu_sent_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuSent;} } }; int metricsInverterId; uint8_t metricsFieldId; @@ -718,21 +727,21 @@ class Web { // So several "Info:" blocks are used to keep the transmission going switch (metricsStep) { case metricsStateStart: // System Info : fit to one packet - snprintf(type,sizeof(type),"# TYPE %sinfo gauge\n",metricPrefix); - snprintf(topic,sizeof(topic),"%sinfo{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n",metricPrefix, + snprintf(type,sizeof(type),"# TYPE %sinfo gauge\n",metricConstPrefix); + snprintf(topic,sizeof(topic),"%sinfo{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n",metricConstPrefix, mApp->getVersion(), mConfig->sys.deviceName); metrics = String(type) + String(topic); - snprintf(type,sizeof(type),"# TYPE %sfreeheap gauge\n",metricPrefix); - snprintf(topic,sizeof(topic),"%sfreeheap{devicename=\"%s\"} %u\n",metricPrefix,mConfig->sys.deviceName,ESP.getFreeHeap()); + snprintf(type,sizeof(type),"# TYPE %sfreeheap gauge\n",metricConstPrefix); + snprintf(topic,sizeof(topic),"%sfreeheap{devicename=\"%s\"} %u\n",metricConstPrefix,mConfig->sys.deviceName,ESP.getFreeHeap()); metrics += String(type) + String(topic); - snprintf(type,sizeof(type),"# TYPE %suptime counter\n",metricPrefix); - snprintf(topic,sizeof(topic),"%suptime{devicename=\"%s\"} %u\n",metricPrefix, mConfig->sys.deviceName, mApp->getUptime()); + snprintf(type,sizeof(type),"# TYPE %suptime counter\n",metricConstPrefix); + snprintf(topic,sizeof(topic),"%suptime{devicename=\"%s\"} %u\n",metricConstPrefix, mConfig->sys.deviceName, mApp->getUptime()); metrics += String(type) + String(topic); - snprintf(type,sizeof(type),"# TYPE %swifi_rssi_db gauge\n",metricPrefix); - snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricPrefix, mConfig->sys.deviceName, WiFi.RSSI()); + snprintf(type,sizeof(type),"# TYPE %swifi_rssi_db gauge\n",metricConstPrefix); + snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricConstPrefix, mConfig->sys.deviceName, WiFi.RSSI()); metrics += String(type) + String(topic); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); @@ -754,8 +763,15 @@ class Web { case metricsStateInverterFrameCnt: case metricsStateInverterTxCnt: case metricsStateInverterRetransmits: - metrics = "# TYPE ahoy_solar_inverter_" + String(inverterMetrics[metricsStep].type) + " gauge\n"; - metrics += inverterMetric(topic, sizeof(topic),(String("ahoy_solar_inverter_") + inverterMetrics[metricsStep].format).c_str(), inverterMetrics[metricsStep].valueFunc); + case metricsStateInverterIvRxCnt: + case metricsStateInverterIvTxCnt: + case metricsStateInverterDtuRxCnt: + case metricsStateInverterDtuTxCnt: + metrics = "# TYPE ahoy_solar_inverter_" + String(inverterMetrics[metricsStep].topic) + " " + String(inverterMetrics[metricsStep].type) + "\n"; + metrics += inverterMetric(topic, sizeof(topic), + (String("ahoy_solar_inverter_") + inverterMetrics[metricsStep].topic + + inverterMetrics[metricsStep].format).c_str(), + inverterMetrics[metricsStep].valueFunc); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); // ugly hack to increment the enum metricsStep = static_cast( static_cast(metricsStep) + 1); @@ -796,7 +812,7 @@ class Web { std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); // Declare metric only once if (channel != 0 && !metricDeclared) { - snprintf(type, sizeof(type), "# TYPE %s%s%s %s\n",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); + snprintf(type, sizeof(type), "# TYPE %s%s%s %s\n",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); metrics += type; metricDeclared = true; } @@ -810,11 +826,11 @@ class Web { strncpy(total,"_total",sizeof(total)); } if (!metricTotalDeclard) { - snprintf(type, sizeof(type), "# TYPE %s%s%s%s %s\n",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str()); + snprintf(type, sizeof(type), "# TYPE %s%s%s%s %s\n",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str()); metrics += type; metricTotalDeclard = true; } - snprintf(topic, sizeof(topic), "%s%s%s%s{inverter=\"%s\"}",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name); + snprintf(topic, sizeof(topic), "%s%s%s%s{inverter=\"%s\"}",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name); } else { // Report (non zero) channel value // Use a fallback channel name (ch0, ch1, ...)if non is given by user @@ -824,7 +840,7 @@ class Web { } else { snprintf(chName,sizeof(chName),"ch%1d",channel); } - snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\",channel=\"%s\"}",metricPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName); + snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\",channel=\"%s\"}",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName); } snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec)); metrics += topic; @@ -833,12 +849,14 @@ class Web { } } if (metrics.length() < 1) { - metrics = "# Info: Field #"+String(metricsFieldId)+" not available for inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; + metrics = "# Info: Field #"+String(metricsFieldId)+" (" + fields[metricsFieldId] + + ") not available for inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; metricsFieldId++; // Process next field Id metricsStep = metricStateRealtimeFieldId; } } else { - metrics = "# Info: No data for field #"+String(metricsFieldId)+" of inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; + metrics = "# Info: No data for field #"+String(metricsFieldId)+ " (" + fields[metricsFieldId] + + ") of inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; metricsFieldId++; // Process next field Id metricsStep = metricStateRealtimeFieldId; } @@ -854,7 +872,7 @@ class Web { case metricsStateAlarmData: // Alarm Info loop : fit to one packet // Perform grouping on metrics according to Prometheus exposition format specification - snprintf(type, sizeof(type),"# TYPE %s%s gauge\n",metricPrefix,fields[FLD_LAST_ALARM_CODE]); + snprintf(type, sizeof(type),"# TYPE %s%s gauge\n",metricConstPrefix,fields[FLD_LAST_ALARM_CODE]); metrics = type; for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { @@ -866,7 +884,7 @@ class Web { alarmChannelId = 0; if (alarmChannelId < rec->length) { std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); - snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\"}",metricPrefix, iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); + snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\"}",metricConstPrefix, iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec)); metrics += topic; metrics += val;