//----------------------------------------------------------------------------- // 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- #ifndef __HM_INVERTER_H__ #define __HM_INVERTER_H__ #if defined(ESP32) && defined(F) #undef F #define F(sl) (sl) #endif #include "hmDefines.h" #include #include #include "../config/settings.h" /** * For values which are of interest and not transmitted by the inverter can be * calculated automatically. * A list of functions can be linked to the assignment and will be executed * automatically. Their result does not differ from original read values. */ // forward declaration of class template class Inverter; // prototypes template static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0); template static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0); template static T calcUdcCh(Inverter<> *iv, uint8_t arg0); template static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0); template static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0); template static T calcIrradiation(Inverter<> *iv, uint8_t arg0); template using func_t = T (Inverter<> *, uint8_t); template struct calcFunc_t { uint8_t funcId; // unique id func_t* func; // function pointer }; template struct record_t { byteAssign_t* assign; // assigment 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 }; class CommandAbstract { public: CommandAbstract(uint8_t txType = 0, uint8_t cmd = 0) { _TxType = txType; _Cmd = cmd; }; virtual ~CommandAbstract() {}; const uint8_t getCmd() { return _Cmd; } protected: uint8_t _TxType; uint8_t _Cmd; }; class InfoCommand : public CommandAbstract { public: InfoCommand(uint8_t cmd){ _TxType = 0x15; _Cmd = cmd; } }; class MiInfoCommand : public CommandAbstract { public: MiInfoCommand(uint8_t cmd){ _TxType = cmd; _Cmd = cmd; } }; // list of all available functions, mapped in hmDefines.h template const calcFunc_t calcFunctions[] = { { CALC_YT_CH0, &calcYieldTotalCh0 }, { CALC_YD_CH0, &calcYieldDayCh0 }, { CALC_UDC_CH, &calcUdcCh }, { CALC_PDC_CH0, &calcPowerDcCh0 }, { CALC_EFF_CH0, &calcEffiencyCh0 }, { CALC_IRR_CH, &calcIrradiation } }; template class Inverter { public: uint8_t ivGen; // generation of inverter (HM / MI) cfgIv_t *config; // stored settings uint8_t id; // unique id uint8_t type; // integer which refers to inverter type uint16_t alarmMesIndex; // Last recorded Alarm Message Index uint16_t powerLimit[2]; // limit power output float actPowerLimit; // actual power limit uint8_t devControlCmd; // carries the requested cmd serial_u radioId; // id converted to modbus uint8_t channels; // number of PV channels (1-4) record_t recordMeas; // structure for measured values record_t recordInfo; // structure for info values record_t recordConfig; // structure for system config values record_t recordAlarm; // structure for alarm values //String lastAlarmMsg; bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null) bool isConnected; // shows if inverter was successfully identified (fw version and hardware info) Inverter() { ivGen = IV_HM; powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited powerLimit[1] = AbsolutNonPersistent; // default power limit setting actPowerLimit = 0xffff; // init feedback from inverter to -1 mDevControlRequest = false; devControlCmd = InitDataState; initialized = false; //lastAlarmMsg = "nothing"; alarmMesIndex = 0; isConnected = false; } ~Inverter() { // TODO: cleanup } template void enqueCommand(uint8_t cmd) { _commandQueue.push(std::make_shared(cmd)); DPRINTLN(DBG_INFO, F("(#") + String(id) + F(") enqueuedCmd: 0x") + String(cmd, HEX)); } void setQueuedCmdFinished() { if (!_commandQueue.empty()) { // Will destroy CommandAbstract Class Object (?) _commandQueue.pop(); } } void clearCmdQueue() { DPRINTLN(DBG_INFO, F("clearCmdQueue")); while (!_commandQueue.empty()) { // Will destroy CommandAbstract Class Object (?) _commandQueue.pop(); } } uint8_t getQueuedCmd() { if (_commandQueue.empty()) { if (ivGen != IV_MI) { if (getFwVersion() == 0) enqueCommand(InverterDevInform_All); // firmware version enqueCommand(RealTimeRunData_Debug); // live data } else if (ivGen == IV_MI){ if (type == INV_TYPE_4CH) { enqueCommand(0x36); /*for(uint8_t i = 0x36; i <= 0x39; i++) { enqueCommand(i); // live data }*/ } else if (type == INV_TYPE_2CH) { enqueCommand(0x09); //enqueCommand(0x11); } else if (type == INV_TYPE_1CH) { enqueCommand(0x09); } //if (getFwVersion() == 0) // enqueCommand(InverterDevInform_All); // firmware version, might not work, esp. for 1/2 ch hardware } if ((actPowerLimit == 0xffff) && isConnected) enqueCommand(SystemConfigPara); // power limit info } return _commandQueue.front().get()->getCmd(); } void init(void) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:init")); initAssignment(&recordMeas, RealTimeRunData_Debug); initAssignment(&recordInfo, InverterDevInform_All); initAssignment(&recordConfig, SystemConfigPara); initAssignment(&recordAlarm, AlarmData); toRadioId(); initialized = true; } uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getPosByChFld")); uint8_t pos = 0; if(NULL != rec) { for(; pos < rec->length; pos++) { if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId)) break; } return (pos >= rec->length) ? 0xff : pos; } else return 0xff; } byteAssign_t *getByteAssign(uint8_t pos, record_t<> *rec) { return &rec->assign[pos]; } const char *getFieldName(uint8_t pos, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getFieldName")); if(NULL != rec) return fields[rec->assign[pos].fieldId]; return notAvail; } const char *getUnit(uint8_t pos, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getUnit")); if(NULL != rec) return units[rec->assign[pos].unitId]; return notAvail; } uint8_t getChannel(uint8_t pos, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getChannel")); if(NULL != rec) return rec->assign[pos].ch; return 0; } bool setDevControlRequest(uint8_t cmd) { if(isConnected) { mDevControlRequest = true; devControlCmd = cmd; } return isConnected; } void clearDevControlRequest() { mDevControlRequest = false; } inline bool getDevControlRequest() { return mDevControlRequest; } void addValue(uint8_t pos, uint8_t buf[], record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:addValue")); if(NULL != rec) { uint8_t ptr = rec->assign[pos].start; uint8_t end = ptr + rec->assign[pos].num; uint16_t div = rec->assign[pos].div; if(NULL != rec) { if(CMD_CALC != div) { uint32_t val = 0; do { val <<= 8; val |= buf[ptr]; } while(++ptr != end); if (FLD_T == rec->assign[pos].fieldId) { // temperature is a signed value! rec->record[pos] = (REC_TYP)((int16_t)val) / (REC_TYP)(div); } else if (FLD_YT == rec->assign[pos].fieldId) { rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div)) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]); } else { if ((REC_TYP)(div) > 1) rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); else rec->record[pos] = (REC_TYP)(val); } } } 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]){ alarmMesIndex = rec->record[pos]; //enqueCommand(AlarmUpdate); // What is the function of AlarmUpdate? DPRINTLN(DBG_INFO, "alarm ID incremented to " + String(alarmMesIndex)); enqueCommand(AlarmData); } } } else if (rec->assign == InfoAssignment) { DPRINTLN(DBG_DEBUG, "add info"); // eg. fw version ... isConnected = true; } else if (rec->assign == SystemConfigParaAssignment) { DPRINTLN(DBG_DEBUG, "add config"); if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){ actPowerLimit = rec->record[pos]; DPRINT(DBG_DEBUG, F("Inverter actual power limit: ") + String(actPowerLimit, 1)); } } else if (rec->assign == AlarmDataAssignment) { DPRINTLN(DBG_DEBUG, "add alarm"); //if (getPosByChFld(0, FLD_LAST_ALARM_CODE, rec) == pos){ // lastAlarmMsg = getAlarmStr(rec->record[pos]); //} } else DPRINTLN(DBG_WARN, F("add with unknown assginment")); } else DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x")); } /*inline REC_TYP getPowerLimit(void) { record_t<> *rec = getRecordStruct(SystemConfigPara); return getChannelFieldValue(CH0, FLD_ACT_ACTIVE_PWR_LIMIT, rec); }*/ bool setValue(uint8_t pos, record_t<> *rec, REC_TYP val) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:setValue")); if(NULL == rec) return false; if(pos > rec->length) return false; rec->record[pos] = val; return true; } REC_TYP getChannelFieldValue(uint8_t channel, uint8_t fieldId, record_t<> *rec) { uint8_t pos = 0; if(NULL != rec) { for(; pos < rec->length; pos++) { if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId)) break; } if(pos >= rec->length) return 0; return rec->record[pos]; } else return 0; } REC_TYP getValue(uint8_t pos, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue")); if(NULL == rec) return 0; if(pos > rec->length) return 0; return rec->record[pos]; } void doCalculations() { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:doCalculations")); record_t<> *rec = getRecordStruct(RealTimeRunData_Debug); for(uint8_t i = 0; i < rec->length; i++) { if(CMD_CALC == rec->assign[i].div) { rec->record[i] = calcFunctions[rec->assign[i].start].func(this, rec->assign[i].num); } yield(); } } bool isAvailable(uint32_t timestamp) { if((timestamp - recordMeas.ts) < INACT_THRES_SEC) return true; if((timestamp - recordInfo.ts) < INACT_THRES_SEC) return true; if((timestamp - recordConfig.ts) < INACT_THRES_SEC) return true; if((timestamp - recordAlarm.ts) < INACT_THRES_SEC) return true; return false; } bool isProducing(uint32_t timestamp) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isProducing")); if(isAvailable(timestamp)) { uint8_t pos = getPosByChFld(CH0, FLD_PAC, &recordMeas); return (getValue(pos, &recordMeas) > INACT_PWR_THRESH); } return false; } uint16_t getFwVersion() { record_t<> *rec = getRecordStruct(InverterDevInform_All); uint8_t pos = getPosByChFld(CH0, FLD_FW_VERSION, rec); return getValue(pos, rec); } uint32_t getLastTs(record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getLastTs")); return rec->ts; } record_t<> *getRecordStruct(uint8_t cmd) { switch (cmd) { case RealTimeRunData_Debug: return &recordMeas; // 11 = 0x0b case InverterDevInform_All: return &recordInfo; // 1 = 0x01 case SystemConfigPara: return &recordConfig; // 5 = 0x05 case AlarmData: return &recordAlarm; // 17 = 0x11 default: break; } return NULL; } void initAssignment(record_t<> *rec, uint8_t cmd) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:initAssignment")); rec->ts = 0; rec->length = 0; switch (cmd) { case RealTimeRunData_Debug: if (INV_TYPE_1CH == type) { rec->length = (uint8_t)(HM1CH_LIST_LEN); rec->assign = (byteAssign_t *)hm1chAssignment; rec->pyldLen = HM1CH_PAYLOAD_LEN; channels = 1; } else if (INV_TYPE_2CH == type) { rec->length = (uint8_t)(HM2CH_LIST_LEN); rec->assign = (byteAssign_t *)hm2chAssignment; rec->pyldLen = HM2CH_PAYLOAD_LEN; channels = 2; } else if (INV_TYPE_4CH == type) { rec->length = (uint8_t)(HM4CH_LIST_LEN); rec->assign = (byteAssign_t *)hm4chAssignment; rec->pyldLen = HM4CH_PAYLOAD_LEN; channels = 4; } else { rec->length = 0; rec->assign = NULL; rec->pyldLen = 0; channels = 0; } break; case InverterDevInform_All: rec->length = (uint8_t)(HMINFO_LIST_LEN); rec->assign = (byteAssign_t *)InfoAssignment; rec->pyldLen = HMINFO_PAYLOAD_LEN; break; case SystemConfigPara: rec->length = (uint8_t)(HMSYSTEM_LIST_LEN); rec->assign = (byteAssign_t *)SystemConfigParaAssignment; rec->pyldLen = HMSYSTEM_PAYLOAD_LEN; break; case AlarmData: rec->length = (uint8_t)(HMALARMDATA_LIST_LEN); rec->assign = (byteAssign_t *)AlarmDataAssignment; rec->pyldLen = HMALARMDATA_PAYLOAD_LEN; break; default: DPRINTLN(DBG_INFO, F("initAssignment: Parser not implemented")); break; } if(0 != rec->length) { rec->record = new REC_TYP[rec->length]; memset(rec->record, 0, sizeof(REC_TYP) * rec->length); } } uint16_t parseAlarmLog(uint8_t id, uint8_t pyld[], uint8_t len, uint32_t *start, uint32_t *endTime) { uint8_t startOff = 2 + id * ALARM_LOG_ENTRY_SIZE; if((startOff + ALARM_LOG_ENTRY_SIZE) > len) return 0; uint16_t wCode = ((uint16_t)pyld[startOff]) << 8 | pyld[startOff+1]; uint32_t startTimeOffset = 0, endTimeOffset = 0; if (((wCode >> 13) & 0x01) == 1) // check if is AM or PM startTimeOffset = 12 * 60 * 60; if (((wCode >> 12) & 0x01) == 1) // check if is AM or PM endTimeOffset = 12 * 60 * 60; *start = (((uint16_t)pyld[startOff + 4] << 8) | ((uint16_t)pyld[startOff + 5])) + startTimeOffset; *endTime = (((uint16_t)pyld[startOff + 6] << 8) | ((uint16_t)pyld[startOff + 7])) + endTimeOffset; DPRINTLN(DBG_INFO, "Alarm #" + String(pyld[startOff+1]) + " '" + String(getAlarmStr(pyld[startOff+1])) + "' start: " + ah::getTimeStr(*start) + ", end: " + ah::getTimeStr(*endTime)); return pyld[startOff+1]; } String getAlarmStr(uint16_t alarmCode) { switch (alarmCode) { // breaks are intentionally missing! case 1: return String(F("Inverter start")); case 2: return String(F("DTU command failed")); case 121: return String(F("Over temperature protection")); case 125: return String(F("Grid configuration parameter error")); case 126: return String(F("Software error code 126")); case 127: return String(F("Firmware error")); case 128: return String(F("Software error code 128")); case 129: return String(F("Software error code 129")); case 130: return String(F("Offline")); case 141: return String(F("Grid overvoltage")); case 142: return String(F("Average grid overvoltage")); case 143: return String(F("Grid undervoltage")); case 144: return String(F("Grid overfrequency")); case 145: return String(F("Grid underfrequency")); case 146: return String(F("Rapid grid frequency change")); case 147: return String(F("Power grid outage")); case 148: return String(F("Grid disconnection")); case 149: return String(F("Island detected")); case 205: return String(F("Input port 1 & 2 overvoltage")); case 206: return String(F("Input port 3 & 4 overvoltage")); case 207: return String(F("Input port 1 & 2 undervoltage")); case 208: return String(F("Input port 3 & 4 undervoltage")); case 209: return String(F("Port 1 no input")); case 210: return String(F("Port 2 no input")); case 211: return String(F("Port 3 no input")); case 212: return String(F("Port 4 no input")); case 213: return String(F("PV-1 & PV-2 abnormal wiring")); case 214: return String(F("PV-3 & PV-4 abnormal wiring")); case 215: return String(F("PV-1 Input overvoltage")); case 216: return String(F("PV-1 Input undervoltage")); case 217: return String(F("PV-2 Input overvoltage")); case 218: return String(F("PV-2 Input undervoltage")); case 219: return String(F("PV-3 Input overvoltage")); case 220: return String(F("PV-3 Input undervoltage")); case 221: return String(F("PV-4 Input overvoltage")); case 222: return String(F("PV-4 Input undervoltage")); case 301: return String(F("Hardware error code 301")); case 302: return String(F("Hardware error code 302")); case 303: return String(F("Hardware error code 303")); case 304: return String(F("Hardware error code 304")); case 305: return String(F("Hardware error code 305")); case 306: return String(F("Hardware error code 306")); case 307: return String(F("Hardware error code 307")); case 308: return String(F("Hardware error code 308")); case 309: return String(F("Hardware error code 309")); case 310: return String(F("Hardware error code 310")); case 311: return String(F("Hardware error code 311")); case 312: return String(F("Hardware error code 312")); case 313: return String(F("Hardware error code 313")); case 314: return String(F("Hardware error code 314")); case 5041: return String(F("Error code-04 Port 1")); case 5042: return String(F("Error code-04 Port 2")); case 5043: return String(F("Error code-04 Port 3")); case 5044: return String(F("Error code-04 Port 4")); case 5051: return String(F("PV Input 1 Overvoltage/Undervoltage")); case 5052: return String(F("PV Input 2 Overvoltage/Undervoltage")); case 5053: return String(F("PV Input 3 Overvoltage/Undervoltage")); case 5054: return String(F("PV Input 4 Overvoltage/Undervoltage")); case 5060: return String(F("Abnormal bias")); case 5070: return String(F("Over temperature protection")); case 5080: return String(F("Grid Overvoltage/Undervoltage")); case 5090: return String(F("Grid Overfrequency/Underfrequency")); case 5100: return String(F("Island detected")); case 5120: return String(F("EEPROM reading and writing error")); case 5150: return String(F("10 min value grid overvoltage")); case 5200: return String(F("Firmware error")); case 8310: return String(F("Shut down")); case 9000: return String(F("Microinverter is suspected of being stolen")); default: return String(F("Unknown")); } } private: void toRadioId(void) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:toRadioId")); radioId.u64 = 0ULL; radioId.b[4] = config->serial.b[0]; radioId.b[3] = config->serial.b[1]; radioId.b[2] = config->serial.b[2]; radioId.b[1] = config->serial.b[3]; radioId.b[0] = 0x01; } std::queue> _commandQueue; bool mDevControlRequest; // true if change needed }; /** * To calculate values which are not transmitted by the unit there is a generic * list of functions which can be linked to the assignment. * The special command 0xff (CMDFF) must be used. */ template static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldTotalCh0")); if(NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); T yield = 0; for(uint8_t i = 1; i <= iv->channels; i++) { uint8_t pos = iv->getPosByChFld(i, FLD_YT, rec); yield += iv->getValue(pos, rec); } return yield; } return 0.0; } template static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldDayCh0")); if(NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); T yield = 0; for(uint8_t i = 1; i <= iv->channels; i++) { uint8_t pos = iv->getPosByChFld(i, FLD_YD, rec); yield += iv->getValue(pos, rec); } return yield; } return 0.0; } template static T calcUdcCh(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcUdcCh")); // arg0 = channel of source record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); for(uint8_t i = 0; i < rec->length; i++) { if((FLD_UDC == rec->assign[i].fieldId) && (arg0 == rec->assign[i].ch)) { return iv->getValue(i, rec); } } return 0.0; } template static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcPowerDcCh0")); if(NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); T dcPower = 0; for(uint8_t i = 1; i <= iv->channels; i++) { uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec); dcPower += iv->getValue(pos, rec); } return dcPower; } return 0.0; } template static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcEfficiencyCh0")); if(NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); uint8_t pos = iv->getPosByChFld(CH0, FLD_PAC, rec); T acPower = iv->getValue(pos, rec); T dcPower = 0; for(uint8_t i = 1; i <= iv->channels; i++) { pos = iv->getPosByChFld(i, FLD_PDC, rec); dcPower += iv->getValue(pos, rec); } if(dcPower > 0) return acPower / dcPower * 100.0f; } return 0.0; } template static T calcIrradiation(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcIrradiation")); // arg0 = channel if(NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); uint8_t pos = iv->getPosByChFld(arg0, FLD_PDC, rec); if(iv->config->chMaxPwr[arg0-1] > 0) return iv->getValue(pos, rec) / iv->config->chMaxPwr[arg0-1] * 100.0f; } return 0.0; } #endif /*__HM_INVERTER_H__*/