//----------------------------------------------------------------------------- // 2022 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 /** * 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; } }; // 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 id; // unique id char name[MAX_NAME_LENGTH]; // human readable name, eg. "HM-600.1" uint8_t type; // integer which refers to inverter type uint16_t alarmMesIndex; // Last recorded Alarm Message Index uint16_t fwVersion; // Firmware Version from Info Command Request uint16_t powerLimit[2]; // limit power output float actPowerLimit; // actual power limit uint8_t devControlCmd; // carries the requested cmd bool devControlRequest; // true if change needed serial_u serial; // serial number as on barcode 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 uint16_t chMaxPwr[4]; // maximum power of the modules (Wp) char chName[4][MAX_NAME_LENGTH]; // human readable name for channels String lastAlarmMsg; bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null) Inverter() { powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited powerLimit[1] = AbsolutNonPersistent; // default power limit setting actPowerLimit = 0xffff; // init feedback from inverter to -1 devControlRequest = false; devControlCmd = InitDataState; initialized = false; fwVersion = 0; lastAlarmMsg = "nothing"; alarmMesIndex = 0; } ~Inverter() { // TODO: cleanup } template void enqueCommand(uint8_t cmd) { _commandQueue.push(std::make_shared(cmd)); DPRINTLN(DBG_INFO, "enqueuedCmd: " + String(cmd)); } void setQueuedCmdFinished() { if (!_commandQueue.empty()) { // Will destroy CommandAbstract Class Object (?) _commandQueue.pop(); } } void clearCmdQueue() { while (!_commandQueue.empty()) { // Will destroy CommandAbstract Class Object (?) _commandQueue.pop(); } } uint8_t getQueuedCmd() { if (_commandQueue.empty()){ // Fill with default commands enqueCommand(RealTimeRunData_Debug); if (fwVersion == 0) { // info needed maybe after "one night" (=> DC>0 to DC=0 and to DC>0) or reboot enqueCommand(InverterDevInform_All); } if (actPowerLimit == 0xffff) { // info needed maybe after "one nigth" (=> DC>0 to DC=0 and to DC>0) or reboot enqueCommand(SystemConfigPara); } } 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(); memset(name, 0, MAX_NAME_LENGTH); memset(chName, 0, MAX_NAME_LENGTH * 4); 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; } 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 ((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? enqueCommand(AlarmData); } else { alarmMesIndex = rec->record[pos]; // no change } } } else if (rec->assign == InfoAssignment) { DPRINTLN(DBG_DEBUG, "add info"); // get at least the firmware version and save it to the inverter object if (getPosByChFld(0, FLD_FW_VERSION, rec) == pos){ fwVersion = rec->record[pos]; DPRINT(DBG_DEBUG, F("Inverter FW-Version: ") + String(fwVersion)); } } else if (rec->assign == SystemConfigParaAssignment) { DPRINTLN(DBG_DEBUG, "add config"); // get at least the firmware version and save it to the inverter object 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")); } REC_TYP getValue(uint8_t pos, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue")); if(NULL == rec) 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, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isAvailable")); return ((timestamp - rec->ts) < INACT_THRES_SEC); } bool isProducing(uint32_t timestamp, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isProducing")); if(isAvailable(timestamp, rec)) { uint8_t pos = getPosByChFld(CH0, FLD_PAC, rec); return (getValue(pos, rec) > INACT_PWR_THRESH); } return false; } 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; case InverterDevInform_All: return &recordInfo; case SystemConfigPara: return &recordConfig; case AlarmData: return &recordAlarm; 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); } } String getAlarmStr(u_int16_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: std::queue> _commandQueue; void toRadioId(void) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:toRadioId")); radioId.u64 = 0ULL; radioId.b[4] = serial.b[0]; radioId.b[3] = serial.b[1]; radioId.b[2] = serial.b[2]; radioId.b[1] = serial.b[3]; radioId.b[0] = 0x01; } }; /** * 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->chMaxPwr[arg0-1] > 0) return iv->getValue(pos, rec) / iv->chMaxPwr[arg0-1] * 100.0f; } return 0.0; } #endif /*__HM_INVERTER_H__*/