//-----------------------------------------------------------------------------
// 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 "../hms/hmsDefines.h"
#include <memory>
#include <queue>
#include <functional>
#include "../config/settings.h"

#include "radio.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.
 */


// prototypes
template<class T=float>
static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0);

template<class T=float>
static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0);

template<class T=float>
static T calcUdcCh(Inverter<> *iv, uint8_t arg0);

template<class T=float>
static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0);

template<class T=float>
static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0);

template<class T=float>
static T calcIrradiation(Inverter<> *iv, uint8_t arg0);

template<class T=float>
static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0);

template<class T=float>
static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0);

template<class T=float>
using func_t = T (Inverter<> *, uint8_t);

template<class T=float>
struct calcFunc_t {
    uint8_t funcId; // unique id
    func_t<T>*  func;   // function pointer
};

template<class T=float>
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
};

struct alarm_t {
    uint16_t code;
    uint32_t start;
    uint32_t end;
    alarm_t(uint16_t c, uint32_t s, uint32_t e) : code(c), start(s), end(e) {}
    alarm_t() : code(0), start(0), end(0) {}
};

// list of all available functions, mapped in hmDefines.h
template<class T=float>
const calcFunc_t<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   },
    { CALC_MPAC_CH0, &calcMaxPowerAcCh0 },
    { CALC_MPDC_CH,  &calcMaxPowerDc    }
};

enum class InverterStatus : uint8_t {
    OFF,
    STARTING,
    PRODUCING,
    WAS_PRODUCING,
    WAS_ON
};

template <class REC_TYP>
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
        bool          powerLimitAck;     // acknowledged power limit (default: false)
        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<REC_TYP> recordMeas;    // structure for measured values
        record_t<REC_TYP> recordInfo;    // structure for info values
        record_t<REC_TYP> recordHwInfo;  // structure for simple (hardware) info values
        record_t<REC_TYP> recordConfig;  // structure for system config values
        record_t<REC_TYP> recordAlarm;   // structure for alarm values
        bool          isConnected;       // shows if inverter was successfully identified (fw version and hardware info)
        InverterStatus status;           // indicates the current inverter status
        std::array<alarm_t, 10> lastAlarm; // holds last 10 alarms
        uint8_t       alarmNxtWrPos;     // indicates the position in array (rolling buffer)
        uint16_t      alarmCnt;          // counts the total number of occurred alarms
        uint16_t      alarmLastId;       // lastId which was received
        int8_t        rssi;              // RSSI
        uint8_t       miMultiParts;      // helper info for MI multiframe msgs
        bool          mGotLastMsg;       // shows if inverter has already finished transmission cycle
        Radio         *radio;            // pointer to associated radio class
        statistics_t  radioStatistics;   // information about transmitted, failed, ... packets
        int8_t        txRfQuality[5];    // heuristics tx quality (check 'Heuristics.h')
        uint8_t       txRfChId;          // RF TX channel id
        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

        static uint32_t *timestamp;      // system timestamp
        static cfgInst_t *generalConfig; // general inverter configuration from setup

        Inverter() {
            ivGen              = IV_HM;
            powerLimit[0]      = 0xffff;               // 65535 W Limit -> unlimited
            powerLimit[1]      = AbsolutNonPersistent; // default power limit setting
            powerLimitAck      = false;
            actPowerLimit      = 0xffff;               // init feedback from inverter to -1
            mDevControlRequest = false;
            devControlCmd      = InitDataState;
            alarmMesIndex      = 0;
            isConnected        = false;
            status             = InverterStatus::OFF;
            alarmNxtWrPos      = 0;
            alarmCnt           = 0;
            alarmLastId        = 0;
            rssi               = -127;
            miMultiParts       = 0;
            mGotLastMsg        = false;
            radio              = NULL;
            commEnabled        = true;

            memset(&radioStatistics, 0, sizeof(statistics_t));
            memset(txRfQuality, -6, 5);

            memset(mOffYD, 0, sizeof(float) * 6);
            memset(mLastYD, 0, sizeof(float) * 6);
        }

        void tickSend(std::function<void(uint8_t cmd, bool isDevControl)> cb) {
            if(mDevControlRequest) {
                cb(devControlCmd, true);
                mDevControlRequest = false;
            } else if (IV_MI != ivGen) {
                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;
                } else
                    cb(RealTimeRunData_Debug, false);    // get live data
            } else {
                if(0 == getFwVersion())
                    cb(0x0f, false);    // get firmware version; for MI, this makes part of polling the device software and hardware version number
                else {
                    record_t<> *rec = getRecordStruct(InverterDevInform_Simple);
                    if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0)
                        cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1
                    else
                        cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false);
                }
            }
        }

        void init(void) {
            DPRINTLN(DBG_VERBOSE, F("hmInverter.h:init"));
            initAssignment(&recordMeas, RealTimeRunData_Debug);
            initAssignment(&recordInfo, InverterDevInform_All);
            initAssignment(&recordHwInfo, InverterDevInform_Simple);
            initAssignment(&recordConfig, SystemConfigPara);
            initAssignment(&recordAlarm, AlarmData);
            toRadioId();
            curCmtFreq = this->config->frequency; // update to frequency read from settings
        }

        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;
        }

        bool setDevCommand(uint8_t cmd) {
            if(isConnected)
                devControlCmd = cmd;
            return isConnected;
        }

        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) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) {
                            // temperature, Qvar, and power factor are a signed values
                            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) * generalConfig->yieldEffiency) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]);
                        } else if (FLD_YD == rec->assign[pos].fieldId) {
                            float actYD = (REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency;
                            uint8_t idx = rec->assign[pos].ch - 1;
                            if (mLastYD[idx] > actYD)
                                mOffYD[idx] += mLastYD[idx];
                            mLastYD[idx] = actYD;
                            rec->record[pos] = mOffYD[idx] + actYD;
                        } 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];

                            DPRINT(DBG_INFO, "alarm ID incremented to ");
                            DBGPRINTLN(String(alarmMesIndex));
                        }
                    }
                }
                else if (rec->assign == InfoAssignment) {
                    DPRINTLN(DBG_DEBUG, "add info");
                    // eg. fw version ...
                    isConnected = true;
                }
                else if (rec->assign == SimpleInfoAssignment) {
                    DPRINTLN(DBG_DEBUG, "add simple info");
                    // eg. hw version ...
                }
                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: "));
                        DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1));
                    }
                }
                else if (rec->assign == AlarmDataAssignment) {
                    DPRINTLN(DBG_DEBUG, "add alarm");
                }
                else
                    DPRINTLN(DBG_WARN, F("add with unknown assignment"));
            }
            else
                DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x"));

            // update status state-machine
            isProducing();
        }

        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;
        }

        uint32_t getChannelFieldValueInt(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
            return (uint32_t) getChannelFieldValue(channel, fieldId, rec);
        }

        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_TYP>[rec->assign[i].start].func(this, rec->assign[i].num);
                }
                yield();
            }
        }

        bool isAvailable() {
            bool avail = false;
            if((*timestamp - recordMeas.ts) < INVERTER_INACT_THRES_SEC)
                avail = true;
            if((*timestamp - recordInfo.ts) < INVERTER_INACT_THRES_SEC)
                avail = true;
            if((*timestamp - recordConfig.ts) < INVERTER_INACT_THRES_SEC)
                avail = true;
            if((*timestamp - recordAlarm.ts) < INVERTER_INACT_THRES_SEC)
                avail = true;

            if(avail) {
                if(status < InverterStatus::PRODUCING)
                    status = InverterStatus::STARTING;
            } else {
                if((*timestamp - recordMeas.ts) > INVERTER_OFF_THRES_SEC) {
                    status = InverterStatus::OFF;
                    actPowerLimit = 0xffff; // power limit will be read once inverter becomes available
                    alarmMesIndex = 0;
                }
                else
                    status = InverterStatus::WAS_ON;
            }

            return avail;
        }

        bool isProducing() {
            bool producing = false;
            DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isProducing"));
            if(isAvailable()) {
                producing = (getChannelFieldValue(CH0, FLD_PAC, &recordMeas) > INACT_PWR_THRESH);

                if(producing)
                    status = InverterStatus::PRODUCING;
                else if(InverterStatus::PRODUCING == status)
                    status = InverterStatus::WAS_PRODUCING;
            }
            return producing;
        }

        uint16_t getFwVersion() {
            record_t<> *rec = getRecordStruct(InverterDevInform_All);
            return getChannelFieldValue(CH0, FLD_FW_VERSION, rec);
        }

        uint16_t getHwVersion() {
            record_t<> *rec = getRecordStruct(InverterDevInform_Simple);
            return getChannelFieldValue(CH0, FLD_HW_VERSION, rec);
        }

        uint16_t getMaxPower() {
            record_t<> *rec = getRecordStruct(InverterDevInform_Simple);
            if(0 == getChannelFieldValue(CH0, FLD_HW_VERSION, rec))
                return 0;

            for(uint8_t i = 0; i < sizeof(devInfo) / sizeof(devInfo_t); i++) {
                if(devInfo[i].hwPart == (getChannelFieldValueInt(CH0, FLD_PART_NUM, rec) >> 8))
                    return devInfo[i].maxPower;
            }
            return 0;
        }

        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_Simple: return &recordHwInfo; //  0 = 0x00
                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) {
                        if((IV_HM == ivGen) || (IV_MI == ivGen)) {
                            rec->length  = (uint8_t)(HM1CH_LIST_LEN);
                            rec->assign  = (byteAssign_t *)hm1chAssignment;
                            rec->pyldLen = HM1CH_PAYLOAD_LEN;
                        } else if(IV_HMS == ivGen) {
                            rec->length  = (uint8_t)(HMS1CH_LIST_LEN);
                            rec->assign  = (byteAssign_t *)hms1chAssignment;
                            rec->pyldLen = HMS1CH_PAYLOAD_LEN;
                        }
                        channels = 1;
                    }
                    else if (INV_TYPE_2CH == type) {
                        if((IV_HM == ivGen) || (IV_MI == ivGen)) {
                            rec->length  = (uint8_t)(HM2CH_LIST_LEN);
                            rec->assign  = (byteAssign_t *)hm2chAssignment;
                            rec->pyldLen = HM2CH_PAYLOAD_LEN;
                        } else if(IV_HMS == ivGen) {
                            rec->length  = (uint8_t)(HMS2CH_LIST_LEN);
                            rec->assign  = (byteAssign_t *)hms2chAssignment;
                            rec->pyldLen = HMS2CH_PAYLOAD_LEN;
                        }
                        channels = 2;
                    }
                    else if (INV_TYPE_4CH == type) {
                        if((IV_HM == ivGen) || (IV_MI == ivGen)) {
                            rec->length  = (uint8_t)(HM4CH_LIST_LEN);
                            rec->assign  = (byteAssign_t *)hm4chAssignment;
                            rec->pyldLen = HM4CH_PAYLOAD_LEN;
                        } else if(IV_HMS == ivGen) {
                            rec->length  = (uint8_t)(HMS4CH_LIST_LEN);
                            rec->assign  = (byteAssign_t *)hms4chAssignment;
                            rec->pyldLen = HMS4CH_PAYLOAD_LEN;
                        }
                        channels = 4;
                    }
                    else if (INV_TYPE_6CH == type) {
                        rec->length  = (uint8_t)(HMT6CH_LIST_LEN);
                        rec->assign  = (byteAssign_t *)hmt6chAssignment;
                        rec->pyldLen = HMT6CH_PAYLOAD_LEN;
                        channels = 6;
                    }
                    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 InverterDevInform_Simple:
                    rec->length  = (uint8_t)(HMSIMPLE_INFO_LIST_LEN);
                    rec->assign  = (byteAssign_t *)SimpleInfoAssignment;
                    rec->pyldLen = HMSIMPLE_INFO_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);
            }
        }

        void resetAlarms() {
            lastAlarm.fill({0, 0, 0});
            alarmNxtWrPos = 0;
            alarmCnt = 0;
            alarmLastId = 0;

            memset(mOffYD, 0, sizeof(float) * 6);
            memset(mLastYD, 0, sizeof(float) * 6);
        }

        uint16_t parseAlarmLog(uint8_t id, uint8_t pyld[], uint8_t len) {
            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;
            uint32_t start, endTime;

            // check if is AM or PM
            startTimeOffset = ((wCode >> 13) & 0x01) * 12 * 60 * 60;
            if (((wCode >> 12) & 0x03) != 0)
                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_DEBUG, "Alarm #" + String(pyld[startOff+1]) + " '" + String(getAlarmStr(pyld[startOff+1])) + "' start: " + ah::getTimeStr(start) + ", end: " + ah::getTimeStr(endTime));
            addAlarm(pyld[startOff+1], start, endTime);

            alarmCnt++;
            alarmLastId = alarmMesIndex;

            return pyld[startOff+1];
        }

        static 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 73:   return String(F("Temperature >80°C")); // https://github.com/tbnobody/OpenDTU/discussions/590#discussioncomment-6049750
                case 121:  return String(F("Over temperature protection"));
                case 124:  return String(F("Shut down by remote control"));
                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("Abnormal bias"));
                case 130:  return String(F("Offline"));
                case 141:  return String(F("Grid: Grid overvoltage"));
                case 142:  return String(F("Grid: 10 min value grid overvoltage"));
                case 143:  return String(F("Grid: Grid undervoltage"));
                case 144:  return String(F("Grid: Grid overfrequency"));
                case 145:  return String(F("Grid: Grid underfrequency"));
                case 146:  return String(F("Grid: Rapid grid frequency change rate"));
                case 147:  return String(F("Grid: Power grid outage"));
                case 148:  return String(F("Grid: Grid disconnection"));
                case 149:  return String(F("Grid: Island detected"));
                case 171:  return String(F("Grid: Abnormal phase difference between phase to phase"));
                case 205:  return String(F("MPPT-A: Input overvoltage"));
                case 206:  return String(F("MPPT-B: Input overvoltage"));
                case 207:  return String(F("MPPT-A: Input undervoltage"));
                case 208:  return String(F("MPPT-B: Input undervoltage"));
                case 209:  return String(F("PV-1: No input"));
                case 210:  return String(F("PV-2: No input"));
                case 211:  return String(F("PV-3: No input"));
                case 212:  return String(F("PV-4: No input"));
                case 213:  return String(F("MPPT-A: PV-1 & PV-2 abnormal wiring"));
                case 214:  return String(F("MPPT-B: PV-3 & PV-4 abnormal wiring"));
                case 215:  return String(F("MPPT-C: 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:
        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;
        }

        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;
        }

    private:
        float mOffYD[6], mLastYD[6];
        bool mDevControlRequest; // true if change needed
};

template <class REC_TYP>
uint32_t *Inverter<REC_TYP>::timestamp {0};
template <class REC_TYP>
cfgInst_t *Inverter<REC_TYP>::generalConfig {0};


/**
 * 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<class T=float>
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++) {
            yield += iv->getChannelFieldValue(i, FLD_YT, rec);
        }
        return yield;
    }
    return 0.0;
}

template<class T=float>
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++) {
            yield += iv->getChannelFieldValue(i, FLD_YD, rec);
        }
        return yield;
    }
    return 0.0;
}

template<class T=float>
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<class T=float>
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++) {
            dcPower += iv->getChannelFieldValue(i, FLD_PDC, rec);
        }
        return dcPower;
    }
    return 0.0;
}

template<class T=float>
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);
        T acPower = iv->getChannelFieldValue(CH0, FLD_PAC, rec);
        T dcPower = 0;
        for(uint8_t i = 1; i <= iv->channels; i++) {
            dcPower += iv->getChannelFieldValue(i, FLD_PDC, rec);
        }
        if(dcPower > 0)
            return acPower / dcPower * 100.0f;
    }
    return 0.0;
}

template<class T=float>
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);
        if(iv->config->chMaxPwr[arg0-1] > 0)
            return iv->getChannelFieldValue(arg0, FLD_PDC, rec) / iv->config->chMaxPwr[arg0-1] * 100.0f;
    }
    return 0.0;
}

template<class T=float>
static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) {
    DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxPowerAcCh0"));
    T acMaxPower = 0.0;
    if(NULL != iv) {
        record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
        T acPower = iv->getChannelFieldValue(arg0, FLD_PAC, rec);

        for(uint8_t i = 0; i < rec->length; i++) {
            if((FLD_MP == rec->assign[i].fieldId) && (0 == rec->assign[i].ch)) {
                acMaxPower = iv->getValue(i, rec);
            }
        }
        if(acPower > acMaxPower)
            return acPower;
    }
    return acMaxPower;
}

template<class T=float>
static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) {
    DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxPowerDc"));
    // arg0 = channel
    T dcMaxPower = 0.0;
    if(NULL != iv) {
        record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
        T dcPower = iv->getChannelFieldValue(arg0, FLD_PDC, rec);

        for(uint8_t i = 0; i < rec->length; i++) {
            if((FLD_MP == rec->assign[i].fieldId) && (arg0 == rec->assign[i].ch)) {
                dcMaxPower = iv->getValue(i, rec);
            }
        }
        if(dcPower > dcMaxPower)
            return dcPower;
    }
    return dcMaxPower;
}

#endif /*__HM_INVERTER_H__*/