Browse Source

0.8.43

* 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
pull/1155/head^2
lumapu 1 year ago
parent
commit
31ecb9620f
  1. 7
      src/CHANGES.md
  2. 1
      src/app.cpp
  3. 4
      src/app.h
  4. 1
      src/appInterface.h
  5. 2
      src/defines.h
  6. 7
      src/hm/Communication.h
  7. 75
      src/hm/hmInverter.h
  8. 32
      src/publisher/pubMqttIvData.h
  9. 1
      src/web/RestApi.h
  10. 6
      src/web/html/index.html
  11. 2
      src/web/html/setup.html
  12. 35
      src/web/html/style.css
  13. 11
      src/web/html/visualization.html

7
src/CHANGES.md

@ -1,5 +1,12 @@
# Development Changes # 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 ## 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 * 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: beautifiying typography, added spaces between value and unit for `/visualization` #1314

1
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.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.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); mSys.setup(&mTimestamp, &mConfig->inst);
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
initInverter(i); initInverter(i);

4
src/app.h

@ -180,10 +180,6 @@ class app : public IApp, public ah::Scheduler {
once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf"); once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf");
} }
void setMqttPowerLimitAck(Inverter<> *iv) {
mMqtt.setPowerLimitAck(iv);
}
bool getMqttIsConnected() { bool getMqttIsConnected() {
return mMqtt.isConnected(); return mMqtt.isConnected();
} }

1
src/appInterface.h

@ -52,7 +52,6 @@ class IApp {
virtual bool getRebootRequestState() = 0; virtual bool getRebootRequestState() = 0;
virtual bool getSettingsValid() = 0; virtual bool getSettingsValid() = 0;
virtual void setMqttDiscoveryFlag() = 0; virtual void setMqttDiscoveryFlag() = 0;
virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0;
virtual bool getMqttIsConnected() = 0; virtual bool getMqttIsConnected() = 0;
virtual bool getNrfEnabled() = 0; virtual bool getNrfEnabled() = 0;

2
src/defines.h

@ -13,7 +13,7 @@
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 8 #define VERSION_MINOR 8
#define VERSION_PATCH 42 #define VERSION_PATCH 43
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {

7
src/hm/Communication.h

@ -19,6 +19,7 @@
#define MAX_BUFFER 250 #define MAX_BUFFER 250
typedef std::function<void(uint8_t, Inverter<> *)> payloadListenerType; typedef std::function<void(uint8_t, Inverter<> *)> payloadListenerType;
typedef std::function<void(Inverter<> *)> powerLimitAckListenerType;
typedef std::function<void(Inverter<> *)> alarmListenerType; typedef std::function<void(Inverter<> *)> alarmListenerType;
class Communication : public CommQueue<> { class Communication : public CommQueue<> {
@ -40,6 +41,10 @@ class Communication : public CommQueue<> {
mCbPayload = cb; mCbPayload = cb;
} }
void addPowerLimitAckListener(powerLimitAckListenerType cb) {
mCbPwrAck = cb;
}
void addAlarmListener(alarmListenerType cb) { void addAlarmListener(alarmListenerType cb) {
mCbAlarm = cb; mCbAlarm = cb;
} }
@ -401,6 +406,7 @@ class Communication : public CommQueue<> {
DBGPRINT(F(" with PowerLimitControl ")); DBGPRINT(F(" with PowerLimitControl "));
DBGPRINTLN(String(q->iv->powerLimit[1])); DBGPRINTLN(String(q->iv->powerLimit[1]));
q->iv->actPowerLimit = 0xffff; // unknown, readback current value q->iv->actPowerLimit = 0xffff; // unknown, readback current value
(mCbPwrAck)(q->iv);
return accepted; return accepted;
} }
@ -921,6 +927,7 @@ class Communication : public CommQueue<> {
uint8_t mMaxFrameId; uint8_t mMaxFrameId;
uint8_t mPayload[MAX_BUFFER]; uint8_t mPayload[MAX_BUFFER];
payloadListenerType mCbPayload = NULL; payloadListenerType mCbPayload = NULL;
powerLimitAckListenerType mCbPwrAck = NULL;
alarmListenerType mCbAlarm = NULL; alarmListenerType mCbAlarm = NULL;
Heuristic mHeu; Heuristic mHeu;
uint32_t mLastEmptyQueueMillis = 0; uint32_t mLastEmptyQueueMillis = 0;

75
src/hm/hmInverter.h

@ -64,13 +64,28 @@ struct calcFunc_t {
func_t<T>* func; // function pointer func_t<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<class T=float> template<class T=float>
struct record_t { struct record_t {
byteAssign_t* assign; // assignment of bytes in payload byteAssign_t* assign; // assignment of bytes in payload
uint8_t length; // length of the assignment list uint8_t length; // length of the assignment list
T *record; // data pointer T *record; // data pointer
uint32_t ts; // timestamp of last received payload uint32_t ts; // timestamp of last received payload
uint8_t pyldLen; // expected payload length for plausibility check uint8_t pyldLen; // expected payload length for plausibility check
MqttSentStatus mqttSentStatus; // indicates the current MqTT sent status
}; };
struct alarm_t { struct alarm_t {
@ -94,14 +109,6 @@ const calcFunc_t<T> calcFunctions[] = {
{ CALC_MPDC_CH, &calcMaxPowerDc } { CALC_MPDC_CH, &calcMaxPowerDc }
}; };
enum class InverterStatus : uint8_t {
OFF,
STARTING,
PRODUCING,
WAS_PRODUCING,
WAS_ON
};
template <class REC_TYP> template <class REC_TYP>
class Inverter { class Inverter {
public: public:
@ -124,26 +131,28 @@ class Inverter {
bool isConnected; // shows if inverter was successfully identified (fw version and hardware info) bool isConnected; // shows if inverter was successfully identified (fw version and hardware info)
InverterStatus status; // indicates the current inverter status InverterStatus status; // indicates the current inverter status
std::array<alarm_t, 10> lastAlarm; // holds last 10 alarms std::array<alarm_t, 10> 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 alarmCnt; // counts the total number of occurred alarms
uint16_t alarmLastId; // lastId which was received 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 miMultiParts; // helper info for MI multiframe msgs
uint8_t outstandingFrames; // helper info to count difference between expected and received frames 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 uint8_t curFrmCnt; // count received frames in current loop
bool mGotLastMsg; // shows if inverter has already finished transmission cycle 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 bool mIsSingleframeReq; // indicates this is a missing single frame request
Radio *radio; // pointer to associated radio class Radio *radio; // pointer to associated radio class
statistics_t radioStatistics; // information about transmitted, failed, ... packets statistics_t radioStatistics; // information about transmitted, failed, ... packets
HeuristicInv heuristics; // heuristic information / logic HeuristicInv heuristics; // heuristic information / logic
uint8_t curCmtFreq; // current used CMT frequency, used to check if freq. was changed during runtime 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 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 uint32_t *timestamp; // system timestamp
static cfgInst_t *generalConfig; // general inverter configuration from setup static cfgInst_t *generalConfig; // general inverter configuration from setup
public:
Inverter() { Inverter() {
ivGen = IV_HM; ivGen = IV_HM;
powerLimit[0] = 0xffff; // 6553.5 W Limit -> unlimited powerLimit[0] = 0xffff; // 6553.5 W Limit -> unlimited
@ -155,7 +164,6 @@ class Inverter {
alarmMesIndex = 0; alarmMesIndex = 0;
isConnected = false; isConnected = false;
status = InverterStatus::OFF; status = InverterStatus::OFF;
alarmNxtWrPos = 0;
alarmCnt = 0; alarmCnt = 0;
alarmLastId = 0; alarmLastId = 0;
rssi = -127; rssi = -127;
@ -165,6 +173,7 @@ class Inverter {
mIsSingleframeReq = false; mIsSingleframeReq = false;
radio = NULL; radio = NULL;
commEnabled = true; commEnabled = true;
tsMaxAcPower = 0;
memset(&radioStatistics, 0, sizeof(statistics_t)); memset(&radioStatistics, 0, sizeof(statistics_t));
memset(heuristics.txRfQuality, -6, 5); memset(heuristics.txRfQuality, -6, 5);
@ -310,11 +319,11 @@ class Inverter {
rec->record[pos] = (REC_TYP)(val); rec->record[pos] = (REC_TYP)(val);
} }
} }
rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
} }
if(rec == &recordMeas) { if(rec == &recordMeas) {
DPRINTLN(DBG_VERBOSE, "add real time"); DPRINTLN(DBG_VERBOSE, "add real time");
// get last alarm message index and save it in the inverter object // get last alarm message index and save it in the inverter object
if (getPosByChFld(0, FLD_EVT, rec) == pos) { if (getPosByChFld(0, FLD_EVT, rec) == pos) {
if (alarmMesIndex < rec->record[pos]) { if (alarmMesIndex < rec->record[pos]) {
@ -498,6 +507,7 @@ class Inverter {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:initAssignment")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:initAssignment"));
rec->ts = 0; rec->ts = 0;
rec->length = 0; rec->length = 0;
rec->mqttSentStatus = MqttSentStatus::DATA_SENT; // nothing new to transmit
switch (cmd) { switch (cmd) {
case RealTimeRunData_Debug: case RealTimeRunData_Debug:
if (INV_TYPE_1CH == type) { if (INV_TYPE_1CH == type) {
@ -582,7 +592,7 @@ class Inverter {
void resetAlarms() { void resetAlarms() {
lastAlarm.fill({0, 0, 0}); lastAlarm.fill({0, 0, 0});
alarmNxtWrPos = 0; mAlarmNxtWrPos = 0;
alarmCnt = 0; alarmCnt = 0;
alarmLastId = 0; alarmLastId = 0;
@ -596,10 +606,18 @@ class Inverter {
uint16_t txCnt = (pyld[2] << 8) + pyld[3]; uint16_t txCnt = (pyld[2] << 8) + pyld[3];
if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past
radioStatistics.ivLoss = mDtuTxCnt - (rxCnt - mIvRxCnt);
radioStatistics.ivSent = mDtuTxCnt; radioStatistics.ivSent = mDtuTxCnt;
radioStatistics.dtuLoss = txCnt - mIvTxCnt - mDtuRxCnt; if (rxCnt < mIvRxCnt) // overflow
radioStatistics.dtuSent = txCnt - mIvTxCnt; 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); DPRINT_IVID(DBG_INFO, id);
DBGPRINT(F("Inv loss: ")); DBGPRINT(F("Inv loss: "));
@ -790,9 +808,9 @@ class Inverter {
private: private:
inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) { inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) {
lastAlarm[alarmNxtWrPos] = alarm_t(code, start, end); lastAlarm[mAlarmNxtWrPos] = alarm_t(code, start, end);
if(++alarmNxtWrPos >= 10) // rolling buffer if(++mAlarmNxtWrPos >= 10) // rolling buffer
alarmNxtWrPos = 0; mAlarmNxtWrPos = 0;
} }
void toRadioId(void) { void toRadioId(void) {
@ -813,6 +831,7 @@ class Inverter {
uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug
uint16_t mIvRxCnt = 0; uint16_t mIvRxCnt = 0;
uint16_t mIvTxCnt = 0; uint16_t mIvTxCnt = 0;
uint8_t mAlarmNxtWrPos = 0; // indicates the position in array (rolling buffer)
public: public:
uint16_t mDtuRxCnt = 0; uint16_t mDtuRxCnt = 0;
@ -948,8 +967,10 @@ static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) {
dcMaxPower = iv->getValue(i, rec); dcMaxPower = iv->getValue(i, rec);
} }
} }
if(dcPower > dcMaxPower) if(dcPower > dcMaxPower) {
iv->tsMaxAcPower = *iv->timestamp;
return dcPower; return dcPower;
}
} }
return dcMaxPower; return dcMaxPower;
} }

32
src/publisher/pubMqttIvData.h

@ -28,7 +28,6 @@ class PubMqttIvData {
mState = IDLE; mState = IDLE;
mZeroValues = false; mZeroValues = false;
memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * sizeof(uint32_t));
mRTRDataHasBeenSent = false; mRTRDataHasBeenSent = false;
mTable[IDLE] = &PubMqttIvData::stateIdle; mTable[IDLE] = &PubMqttIvData::stateIdle;
@ -102,7 +101,7 @@ class PubMqttIvData {
mPos = 0; mPos = 0;
if(found) { if(found) {
record_t<> *rec = mIv->getRecordStruct(mCmd); 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(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", mIv->config->name);
snprintf(mVal, 40, "%d", mIv->getLastTs(rec)); snprintf(mVal, 40, "%d", mIv->getLastTs(rec));
mPublish(mSubTopic, mVal, true, QOS_0); mPublish(mSubTopic, mVal, true, QOS_0);
@ -112,13 +111,14 @@ class PubMqttIvData {
snprintf(mVal, 40, "%d", mIv->rssi); snprintf(mVal, 40, "%d", mIv->rssi);
mPublish(mSubTopic, mVal, false, QOS_0); mPublish(mSubTopic, mVal, false, QOS_0);
} }
rec->mqttSentStatus = MqttSentStatus::LAST_SUCCESS_SENT;
} }
mIv->isProducing(); // recalculate status mIv->isProducing(); // recalculate status
mState = SEND_DATA; mState = SEND_DATA;
} else if(mSendTotals && mTotalFound) } else if(mSendTotals && mTotalFound) {
mState = SEND_TOTALS; mState = SEND_TOTALS;
else { } else {
mSendList->pop(); mSendList->pop();
mZeroValues = false; mZeroValues = false;
mState = START; mState = START;
@ -132,12 +132,8 @@ class PubMqttIvData {
DPRINT(DBG_WARN, "unknown record to publish!"); DPRINT(DBG_WARN, "unknown record to publish!");
return; 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) { if(mPos < rec->length) {
bool retained = false; bool retained = false;
if (mCmd == RealTimeRunData_Debug) { if (mCmd == RealTimeRunData_Debug) {
@ -172,21 +168,16 @@ class PubMqttIvData {
} else } else
mAllTotalFound = false; 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++; mPos++;
} else { } else {
sendRadioStat(rec->length); sendRadioStat(rec->length);
rec->mqttSentStatus = MqttSentStatus::DATA_SENT;
mState = FIND_NXT_IV; mState = FIND_NXT_IV;
} }
} else } else
@ -263,7 +254,6 @@ class PubMqttIvData {
Inverter<> *mIv, *mIvSend; Inverter<> *mIv, *mIvSend;
uint8_t mPos; uint8_t mPos;
uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS];
bool mRTRDataHasBeenSent; bool mRTRDataHasBeenSent;
char mSubTopic[32 + MAX_NAME_LENGTH + 1]; char mSubTopic[32 + MAX_NAME_LENGTH + 1];

1
src/web/RestApi.h

@ -485,6 +485,7 @@ class RestApi {
obj[F("status")] = (uint8_t)iv->getStatus(); obj[F("status")] = (uint8_t)iv->getStatus();
obj[F("alarm_cnt")] = iv->alarmCnt; obj[F("alarm_cnt")] = iv->alarmCnt;
obj[F("rssi")] = iv->rssi; obj[F("rssi")] = iv->rssi;
obj[F("ts_max_ac_pwr")] = iv->tsMaxAcPower;
JsonArray ch = obj.createNestedArray("ch"); JsonArray ch = obj.createNestedArray("ch");

6
src/web/html/index.html

@ -99,17 +99,17 @@
} }
if(obj.disNightComm) { 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)) { && ((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')); commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE'));
} }
else { else {
commInfo = "Night time, inverter polling disabled, "; 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')); commInfo += "paused at " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE'));
} }
else { 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'));
} }
} }
} }

2
src/web/html/setup.html

@ -765,7 +765,7 @@
ml("div", {class: "col-2"}, cbDisNightCom) ml("div", {class: "col-2"}, cbDisNightCom)
]), ]),
ml("div", {class: "row mb-3"}, [ 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) ml("div", {class: "col-2"}, cbAddTotal)
]) ])
]), ]),

35
src/web/html/style.css

@ -650,39 +650,24 @@ div.hr {
} }
.css-tooltip{ .tooltip{
position: relative; position: relative;
} }
.css-tooltip:hover:after{ .tooltip:hover:after {
content:attr(data-tooltip); content: attr(data);
background:#000; background: var(--nav-active);
padding:5px; padding: 5px;
border-radius:3px; border-radius: 3px;
display: inline-block; display: inline-block;
position: absolute; position: absolute;
transform: translate(-50%,-100%); transform: translate(-50%,-100%);
margin:0 auto; margin:0 auto;
color:#FFF; color: var(--fg2);
min-width:100px; min-width: 100px;
min-width:150px; top: -5px;
top:-5px;
left: 50%; left: 50%;
text-align:center; text-align:center;
} font-size: 1rem;
.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);
} }
#modal { #modal {

11
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"}, [ return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [
ml("div", {class: "row"}, ml("div", {class: "row"},
ml("div", {class: "col"}, [ 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("span", {class: "fs-8 mx-1"}, unit)
])), ])
),
ml("div", {class: "row"}, ml("div", {class: "row"},
ml("div", {class: "col"}, ml("div", {class: "col"},
ml("span", {class: "fs-9"}, des) ml("span", {class: "fs-9"}, des)
@ -108,6 +109,8 @@
if(0 != obj.max_pwr) if(0 != obj.max_pwr)
pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "&nbsp;W"; pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "&nbsp;W";
} }
var maxAcPwr = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000));
return ml("div", {class: "row mt-2"}, return ml("div", {class: "row mt-2"},
ml("div", {class: "col"}, [ ml("div", {class: "col"}, [
ml("div", {class: "p-2 " + clh}, ml("div", {class: "p-2 " + clh},
@ -133,7 +136,7 @@
]), ]),
ml("div", {class: "hr"}), ml("div", {class: "hr"}),
ml("div", {class: "row mt-2"},[ 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][8], "W", "DC Power"),
numMid(obj.ch[0][0], "V", "AC Voltage"), numMid(obj.ch[0][0], "V", "AC Voltage"),
numMid(obj.ch[0][1], "A", "AC Current"), numMid(obj.ch[0][1], "A", "AC Current"),

Loading…
Cancel
Save