|
@ -627,14 +627,22 @@ class Web { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#ifdef ENABLE_PROMETHEUS_EP |
|
|
#ifdef ENABLE_PROMETHEUS_EP |
|
|
|
|
|
// Note
|
|
|
|
|
|
// Prometheus exposition format is defined here: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md
|
|
|
|
|
|
// TODO: Check packetsize for MAX_NUM_INVERTERS. Successfull Tested with 4 Inverters (each with 4 channels)
|
|
|
enum { |
|
|
enum { |
|
|
metricsStateStart, metricsStateInverter, metricStateRealtimeData,metricsStateAlarmData,metricsStateEnd |
|
|
metricsStateStart, |
|
|
|
|
|
metricsStateInverter1, metricsStateInverter2, metricsStateInverter3, metricsStateInverter4, |
|
|
|
|
|
metricStateRealtimeFieldId, metricStateRealtimeInverterId, |
|
|
|
|
|
metricsStateAlarmData, |
|
|
|
|
|
metricsStateEnd |
|
|
} metricsStep; |
|
|
} metricsStep; |
|
|
int metricsInverterId,metricsChannelId; |
|
|
int metricsInverterId; |
|
|
|
|
|
uint8_t metricsFieldId; |
|
|
|
|
|
bool metricDeclared; |
|
|
|
|
|
|
|
|
void showMetrics(AsyncWebServerRequest *request) { |
|
|
void showMetrics(AsyncWebServerRequest *request) { |
|
|
DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); |
|
|
DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); |
|
|
|
|
|
|
|
|
metricsStep = metricsStateStart; |
|
|
metricsStep = metricsStateStart; |
|
|
AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"), |
|
|
AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"), |
|
|
[this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t |
|
|
[this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t |
|
@ -647,7 +655,11 @@ class Web { |
|
|
char type[60], topic[100], val[25]; |
|
|
char type[60], topic[100], val[25]; |
|
|
size_t len = 0; |
|
|
size_t len = 0; |
|
|
int alarmChannelId; |
|
|
int alarmChannelId; |
|
|
|
|
|
int metricsChannelId; |
|
|
|
|
|
|
|
|
|
|
|
// Perform grouping on metrics according to format specification
|
|
|
|
|
|
// Each step must return at least one character. Otherwise the processing of AsyncWebServerResponse stops.
|
|
|
|
|
|
// So several "Info:" blocks are used to keep the transmission going
|
|
|
switch (metricsStep) { |
|
|
switch (metricsStep) { |
|
|
case metricsStateStart: // System Info & NRF Statistics : fit to one packet
|
|
|
case metricsStateStart: // System Info & NRF Statistics : fit to one packet
|
|
|
snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n"); |
|
|
snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n"); |
|
@ -676,93 +688,138 @@ class Web { |
|
|
metrics += radioStatistic(F("tx_cnt"), mSys->Radio.mSendCnt); |
|
|
metrics += radioStatistic(F("tx_cnt"), mSys->Radio.mSendCnt); |
|
|
|
|
|
|
|
|
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); |
|
|
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); |
|
|
// Start Inverter loop
|
|
|
// Next is Inverter information
|
|
|
metricsInverterId = 0; |
|
|
metricsInverterId = 0; |
|
|
metricsStep = metricsStateInverter; |
|
|
metricsStep = metricsStateInverter1; |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case metricsStateInverter: // Inverter loop
|
|
|
case metricsStateInverter1: // Information about all inverters configured : fit to one packet
|
|
|
if (metricsInverterId < mSys->getNumInverters()) { |
|
|
metrics = "# TYPE ahoy_solar_inverter_info gauge\n"; |
|
|
iv = mSys->getInverterByPos(metricsInverterId); |
|
|
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", |
|
|
if(NULL != iv) { |
|
|
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->serial.u64;}); |
|
|
// Inverter info : fit to one packet
|
|
|
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); |
|
|
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_info gauge\n"); |
|
|
metricsStep = metricsStateInverter2; |
|
|
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", |
|
|
break; |
|
|
iv->config->name, iv->config->serial.u64); |
|
|
|
|
|
metrics = String(type) + String(topic); |
|
|
case metricsStateInverter2: // Information about all inverters configured : fit to one packet
|
|
|
|
|
|
metrics += "# TYPE ahoy_solar_inverter_is_enabled gauge\n"; |
|
|
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_enabled gauge\n"); |
|
|
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n", |
|
|
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",iv->config->name,iv->config->enabled); |
|
|
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->enabled;}); |
|
|
metrics += String(type) + String(topic); |
|
|
|
|
|
|
|
|
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); |
|
|
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_available gauge\n"); |
|
|
metricsStep = metricsStateInverter3; |
|
|
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",iv->config->name,iv->isAvailable(mApp->getTimestamp())); |
|
|
break; |
|
|
metrics += String(type) + String(topic); |
|
|
|
|
|
|
|
|
case metricsStateInverter3: // Information about all inverters configured : fit to one packet
|
|
|
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_producing gauge\n"); |
|
|
metrics += "# TYPE ahoy_solar_inverter_is_available gauge\n"; |
|
|
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",iv->config->name,iv->isProducing(mApp->getTimestamp())); |
|
|
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n", |
|
|
metrics += String(type) + String(topic); |
|
|
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isAvailable(mApp->getTimestamp());}); |
|
|
|
|
|
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); |
|
|
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); |
|
|
metricsStep = metricsStateInverter4; |
|
|
|
|
|
break; |
|
|
// Start Realtime Data Channel loop for this inverter
|
|
|
|
|
|
metricsChannelId = 0; |
|
|
case metricsStateInverter4: // Information about all inverters configured : fit to one packet
|
|
|
metricsStep = metricStateRealtimeData; |
|
|
metrics += "# TYPE ahoy_solar_inverter_is_producing gauge\n"; |
|
|
} |
|
|
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n", |
|
|
|
|
|
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isProducing(mApp->getTimestamp());}); |
|
|
|
|
|
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); |
|
|
|
|
|
// Start Realtime Field loop
|
|
|
|
|
|
metricsFieldId = FLD_UDC; |
|
|
|
|
|
metricsStep = metricStateRealtimeFieldId; |
|
|
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
case metricStateRealtimeFieldId: // Iterate over all defined fields
|
|
|
|
|
|
if (metricsFieldId < FLD_LAST_ALARM_CODE) { |
|
|
|
|
|
metrics = "# Info: processing realtime field #"+String(metricsFieldId)+"\n"; |
|
|
|
|
|
metricDeclared = false; |
|
|
|
|
|
|
|
|
|
|
|
metricsInverterId = 0; |
|
|
|
|
|
metricsStep = metricStateRealtimeInverterId; |
|
|
} else { |
|
|
} else { |
|
|
metricsStep = metricsStateEnd; |
|
|
metrics = "# Info: all realtime fields processed\n"; |
|
|
|
|
|
metricsStep = metricsStateAlarmData; |
|
|
} |
|
|
} |
|
|
|
|
|
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case metricStateRealtimeData: // Realtime Data Channel loop
|
|
|
case metricStateRealtimeInverterId: // Iterate over all inverters for this field
|
|
|
iv = mSys->getInverterByPos(metricsInverterId); |
|
|
metrics = ""; |
|
|
rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|
|
if (metricsInverterId < mSys->getNumInverters()) { |
|
|
if (metricsChannelId < rec->length) { |
|
|
// process all channels of this inverter
|
|
|
uint8_t channel = rec->assign[metricsChannelId].ch; |
|
|
|
|
|
// Skip entry if maxPwr is 0 and it's not the inverter channel (channel 0)
|
|
|
iv = mSys->getInverterByPos(metricsInverterId); |
|
|
if (0 == channel || 0 != iv->config->chMaxPwr[channel-1]) { |
|
|
if (NULL != iv) { |
|
|
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); |
|
|
rec = iv->getRecordStruct(RealTimeRunData_Debug); |
|
|
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); |
|
|
for (metricsChannelId=0; metricsChannelId < rec->length;metricsChannelId++) { |
|
|
if (0 == channel) { |
|
|
uint8_t channel = rec->assign[metricsChannelId].ch; |
|
|
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); |
|
|
|
|
|
} else { |
|
|
// Try inverter channel (channel 0) or any channel with maxPwr > 0
|
|
|
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); |
|
|
if (0 == channel || 0 != iv->config->chMaxPwr[channel-1]) { |
|
|
|
|
|
|
|
|
|
|
|
if (metricsFieldId == iv->getByteAssign(metricsChannelId, rec)->fieldId) { |
|
|
|
|
|
// This is the correct field to report
|
|
|
|
|
|
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); |
|
|
|
|
|
// Declare metric only once
|
|
|
|
|
|
if (!metricDeclared) { |
|
|
|
|
|
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); |
|
|
|
|
|
metrics += type; |
|
|
|
|
|
metricDeclared = true; |
|
|
|
|
|
} |
|
|
|
|
|
// report value
|
|
|
|
|
|
if (0 == channel) { |
|
|
|
|
|
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); |
|
|
|
|
|
} else { |
|
|
|
|
|
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); |
|
|
|
|
|
} |
|
|
|
|
|
snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec)); |
|
|
|
|
|
metrics += topic; |
|
|
|
|
|
metrics += val; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
if (metrics.length() < 1) { |
|
|
|
|
|
metrics = "# Info: Field #"+String(metricsFieldId)+" not available for inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; |
|
|
|
|
|
metricsFieldId++; // Process next field Id
|
|
|
|
|
|
metricsStep = metricStateRealtimeFieldId; |
|
|
} |
|
|
} |
|
|
snprintf(val, sizeof(val), "%.3f", iv->getValue(metricsChannelId, rec)); |
|
|
|
|
|
len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); |
|
|
|
|
|
} else { |
|
|
} else { |
|
|
len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends.
|
|
|
metrics = "# Info: No data for field #"+String(metricsFieldId)+" of inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; |
|
|
|
|
|
metricsFieldId++; // Process next field Id
|
|
|
|
|
|
metricsStep = metricStateRealtimeFieldId; |
|
|
} |
|
|
} |
|
|
|
|
|
// Stay in this state and try next inverter
|
|
|
metricsChannelId++; |
|
|
metricsInverterId++; |
|
|
} else { |
|
|
} else { |
|
|
len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends.
|
|
|
metrics = "# Info: All inverters for field #"+String(metricsFieldId)+" processed.\n"; |
|
|
|
|
|
metricsFieldId++; // Process next field Id
|
|
|
// All realtime data channels processed --> try alarm data
|
|
|
metricsStep = metricStateRealtimeFieldId; |
|
|
metricsStep = metricsStateAlarmData; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case metricsStateAlarmData: // Alarm Info loop
|
|
|
case metricsStateAlarmData: // Alarm Info loop : fit to one packet
|
|
|
iv = mSys->getInverterByPos(metricsInverterId); |
|
|
// Perform grouping on metrics according to Prometheus exposition format specification
|
|
|
rec = iv->getRecordStruct(AlarmData); |
|
|
snprintf(type, sizeof(type),"# TYPE ahoy_solar_%s gauge\n",fields[FLD_LAST_ALARM_CODE]); |
|
|
// simple hack : there is only one channel with alarm data
|
|
|
metrics = type; |
|
|
// TODO: find the right one channel with the alarm id
|
|
|
|
|
|
alarmChannelId = 0; |
|
|
for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { |
|
|
// printf("AlarmData Length %d\n",rec->length);
|
|
|
iv = mSys->getInverterByPos(metricsInverterId); |
|
|
if (alarmChannelId < rec->length) { |
|
|
if (NULL != iv) { |
|
|
//uint8_t channel = rec->assign[alarmChannelId].ch;
|
|
|
rec = iv->getRecordStruct(AlarmData); |
|
|
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); |
|
|
// simple hack : there is only one channel with alarm data
|
|
|
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str()); |
|
|
// TODO: find the right one channel with the alarm id
|
|
|
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); |
|
|
alarmChannelId = 0; |
|
|
snprintf(val, sizeof(val), "%.3f", iv->getValue(alarmChannelId, rec)); |
|
|
if (alarmChannelId < rec->length) { |
|
|
len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); |
|
|
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); |
|
|
} else { |
|
|
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); |
|
|
len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends.
|
|
|
snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec)); |
|
|
|
|
|
metrics += topic; |
|
|
|
|
|
metrics += val; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
// alarm channel processed --> try next inverter
|
|
|
len = snprintf((char*)buffer,maxLen,"%s",metrics.c_str()); |
|
|
metricsInverterId++; |
|
|
metricsStep = metricsStateEnd; |
|
|
metricsStep = metricsStateInverter; |
|
|
|
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case metricsStateEnd: |
|
|
case metricsStateEnd: |
|
@ -775,6 +832,21 @@ class Web { |
|
|
request->send(response); |
|
|
request->send(response); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Traverse all inverters and collect the metric via valueFunc
|
|
|
|
|
|
String inverterMetric(char *buffer, size_t len, const char *format, std::function<uint64_t(Inverter<> *iv, IApp *mApp)> valueFunc) { |
|
|
|
|
|
Inverter<> *iv; |
|
|
|
|
|
String metric = ""; |
|
|
|
|
|
for (int metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { |
|
|
|
|
|
iv = mSys->getInverterByPos(metricsInverterId); |
|
|
|
|
|
if (NULL != iv) { |
|
|
|
|
|
snprintf(buffer,len,format,iv->config->name, valueFunc(iv,mApp)); |
|
|
|
|
|
metric += String(buffer); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return metric; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
String radioStatistic(String statistic, uint32_t value) { |
|
|
String radioStatistic(String statistic, uint32_t value) { |
|
|
char type[60], topic[80], val[25]; |
|
|
char type[60], topic[80], val[25]; |
|
|
snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str()); |
|
|
snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str()); |
|
|