mirror of
https://github.com/lumapu/ahoy.git
synced 2025-06-03 03:01:40 +02:00
Merge branch 'grindylow:main' into main
This commit is contained in:
commit
4fc12eb95b
18 changed files with 785 additions and 392 deletions
|
@ -23,7 +23,8 @@ app::app() : Main() {
|
|||
mSerialValues = true;
|
||||
mSerialDebug = false;
|
||||
|
||||
memset(mPacketIds, 0, sizeof(uint32_t)*DBG_CMD_LIST_LEN);
|
||||
memset(mPayload, 0, (MAX_NUM_INVERTERS * sizeof(invPayload_t)));
|
||||
mRxFailed = 0;
|
||||
|
||||
mSys = new HmSystemType();
|
||||
}
|
||||
|
@ -50,21 +51,20 @@ void app::setup(uint32_t timeout) {
|
|||
if(mSettingsValid) {
|
||||
uint64_t invSerial;
|
||||
char invName[MAX_NAME_LENGTH + 1] = {0};
|
||||
uint8_t invType;
|
||||
|
||||
// inverter
|
||||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
|
||||
mEep->read(ADDR_INV_ADDR + (i * 8), &invSerial);
|
||||
mEep->read(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), invName, MAX_NAME_LENGTH);
|
||||
mEep->read(ADDR_INV_TYPE + i, &invType);
|
||||
if(0ULL != invSerial) {
|
||||
mSys->addInverter(invName, invSerial, invType);
|
||||
DPRINTLN("add inverter: " + String(invName) + ", SN: " + String(invSerial, HEX) + ", type: " + String(invType));
|
||||
mSys->addInverter(invName, invSerial);
|
||||
DPRINTLN("add inverter: " + String(invName) + ", SN: " + String(invSerial, HEX));
|
||||
}
|
||||
}
|
||||
mEep->read(ADDR_INV_INTERVAL, &mSendInterval);
|
||||
if(mSendInterval < 5)
|
||||
mSendInterval = 5;
|
||||
mSendTicker = mSendInterval;
|
||||
|
||||
// pinout
|
||||
mEep->read(ADDR_PINOUT, &mSys->Radio.pinCs);
|
||||
|
@ -84,6 +84,7 @@ void app::setup(uint32_t timeout) {
|
|||
mSerialDebug = (tmp == 0x01);
|
||||
if(mSerialInterval < 1)
|
||||
mSerialInterval = 1;
|
||||
mSys->Radio.mSerialDebug = mSerialDebug;
|
||||
|
||||
|
||||
// mqtt
|
||||
|
@ -134,44 +135,43 @@ void app::loop(void) {
|
|||
Main::loop();
|
||||
|
||||
if(checkTicker(&mRxTicker, 5)) {
|
||||
mSys->Radio.switchRxCh();
|
||||
bool rxRdy = mSys->Radio.switchRxCh();
|
||||
|
||||
if(!mSys->BufCtrl.empty()) {
|
||||
uint8_t len, rptCnt;
|
||||
uint8_t len;
|
||||
packet_t *p = mSys->BufCtrl.getBack();
|
||||
|
||||
//if(mSerialDebug)
|
||||
// mSys->Radio.dumpBuf("RAW ", p->packet, MAX_RF_PAYLOAD_SIZE);
|
||||
|
||||
if(mSys->Radio.checkPaketCrc(p->packet, &len, &rptCnt, p->rxCh)) {
|
||||
if(mSys->Radio.checkPaketCrc(p->packet, &len, p->rxCh)) {
|
||||
// process buffer only on first occurrence
|
||||
if((0 != len) && (0 == rptCnt)) {
|
||||
uint8_t *packetId = &p->packet[9];
|
||||
//DPRINTLN("CMD " + String(*packetId, HEX));
|
||||
if(mSerialDebug)
|
||||
mSys->Radio.dumpBuf("Payload ", p->packet, len);
|
||||
if(mSerialDebug) {
|
||||
DPRINT("Received " + String(len) + " bytes channel " + String(p->rxCh) + ": ");
|
||||
mSys->Radio.dumpBuf(NULL, p->packet, len);
|
||||
}
|
||||
|
||||
if(0 != len) {
|
||||
Inverter<> *iv = mSys->findInverter(&p->packet[1]);
|
||||
if(NULL != iv) {
|
||||
for(uint8_t i = 0; i < iv->listLen; i++) {
|
||||
if(iv->assign[i].cmdId == *packetId)
|
||||
iv->addValue(i, &p->packet[9]);
|
||||
uint8_t *pid = &p->packet[9];
|
||||
if((*pid & 0x7F) < 5) {
|
||||
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], len-11);
|
||||
mPayload[iv->id].len[(*pid & 0x7F) - 1] = len-11;
|
||||
}
|
||||
iv->doCalculations();
|
||||
//memcpy(mPayload[(*packetId & 0x7F) - 1], &p->packet[9], MAX_RF_PAYLOAD_SIZE - 11);
|
||||
}
|
||||
|
||||
if(*packetId == 0x01) mPacketIds[0]++;
|
||||
else if(*packetId == 0x02) mPacketIds[1]++;
|
||||
else if(*packetId == 0x03) mPacketIds[2]++;
|
||||
else if(*packetId == 0x81) mPacketIds[3]++;
|
||||
else if(*packetId == 0x82) mPacketIds[4]++;
|
||||
else if(*packetId == 0x83) mPacketIds[5]++;
|
||||
else if(*packetId == 0x84) mPacketIds[6]++;
|
||||
else mPacketIds[7]++;
|
||||
if((*pid & 0x80) == 0x80) {
|
||||
if((*pid & 0x7f) > mPayload[iv->id].maxPackId)
|
||||
mPayload[iv->id].maxPackId = (*pid & 0x7f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mSys->BufCtrl.popBack();
|
||||
}
|
||||
|
||||
|
||||
if(rxRdy) {
|
||||
processPayload(true);
|
||||
}
|
||||
}
|
||||
|
||||
if(checkTicker(&mTicker, 1000)) {
|
||||
|
@ -220,14 +220,34 @@ void app::loop(void) {
|
|||
if(++mSendTicker >= mSendInterval) {
|
||||
mSendTicker = 0;
|
||||
|
||||
if(!mSys->BufCtrl.empty())
|
||||
DPRINTLN("recbuf not empty! #" + String(mSys->BufCtrl.getFill()));
|
||||
Inverter<> *inv;
|
||||
if(!mSys->BufCtrl.empty()) {
|
||||
if(mSerialDebug)
|
||||
DPRINTLN("recbuf not empty! #" + String(mSys->BufCtrl.getFill()));
|
||||
}
|
||||
Inverter<> *iv;
|
||||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
|
||||
inv = mSys->getInverterByPos(i);
|
||||
if(NULL != inv) {
|
||||
iv = mSys->getInverterByPos(i);
|
||||
if(NULL != iv) {
|
||||
// reset payload data
|
||||
memset(mPayload[iv->id].len, 0, MAX_PAYLOAD_ENTRIES);
|
||||
mPayload[iv->id].maxPackId = 0;
|
||||
if(mSerialDebug) {
|
||||
if(!mPayload[iv->id].complete)
|
||||
processPayload(false);
|
||||
|
||||
if(!mPayload[iv->id].complete) {
|
||||
DPRINT("Inverter #" + String(iv->id) + " ");
|
||||
DPRINTLN("no Payload received!");
|
||||
mRxFailed++;
|
||||
}
|
||||
}
|
||||
mPayload[iv->id].complete = false;
|
||||
mPayload[iv->id].ts = mTimestamp;
|
||||
|
||||
yield();
|
||||
mSys->Radio.sendTimePacket(inv->radioId.u64, mTimestamp);
|
||||
if(mSerialDebug)
|
||||
DPRINTLN("Requesting Inverter SN " + String(iv->serial.u64, HEX));
|
||||
mSys->Radio.sendTimePacket(iv->radioId.u64, mPayload[iv->id].ts);
|
||||
mRxTicker = 0;
|
||||
}
|
||||
}
|
||||
|
@ -242,6 +262,80 @@ void app::handleIntr(void) {
|
|||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
bool app::buildPayload(uint8_t id) {
|
||||
//DPRINTLN("Payload");
|
||||
uint16_t crc = 0xffff, crcRcv;
|
||||
if(mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES)
|
||||
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES;
|
||||
|
||||
for(uint8_t i = 0; i < mPayload[id].maxPackId; i ++) {
|
||||
if(mPayload[id].len[i] > 0) {
|
||||
if(i == (mPayload[id].maxPackId-1)) {
|
||||
crc = crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc);
|
||||
crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8)
|
||||
| (mPayload[id].data[i][mPayload[id].len[i] - 1]);
|
||||
}
|
||||
else
|
||||
crc = crc16(mPayload[id].data[i], mPayload[id].len[i], crc);
|
||||
}
|
||||
}
|
||||
if(crc == crcRcv)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
void app::processPayload(bool retransmit) {
|
||||
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
|
||||
Inverter<> *iv = mSys->getInverterByPos(id);
|
||||
if(NULL != iv) {
|
||||
if(!mPayload[iv->id].complete) {
|
||||
if(!buildPayload(iv->id)) {
|
||||
if(retransmit) {
|
||||
if(mPayload[iv->id].maxPackId != 0) {
|
||||
for(uint8_t i = 0; i < (mPayload[iv->id].maxPackId-1); i ++) {
|
||||
if(mPayload[iv->id].len[i] == 0) {
|
||||
if(mSerialDebug)
|
||||
DPRINTLN("Error while retrieving data: Frame " + String(i+1) + " missing: Request Retransmit");
|
||||
mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x15, (0x81+i), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(mSerialDebug)
|
||||
DPRINTLN("Error while retrieving data: last frame missing: Request Retransmit");
|
||||
mSys->Radio.sendTimePacket(iv->radioId.u64, mPayload[iv->id].ts);
|
||||
}
|
||||
mSys->Radio.switchRxCh(100);
|
||||
}
|
||||
}
|
||||
else {
|
||||
mPayload[iv->id].complete = true;
|
||||
uint8_t payload[128] = {0};
|
||||
uint8_t offs = 0;
|
||||
for(uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i ++) {
|
||||
memcpy(&payload[offs], mPayload[iv->id].data[i], (mPayload[iv->id].len[i]));
|
||||
offs += (mPayload[iv->id].len[i]);
|
||||
}
|
||||
offs-=2;
|
||||
if(mSerialDebug) {
|
||||
DPRINT("Payload (" + String(offs) + "): ");
|
||||
mSys->Radio.dumpBuf(NULL, payload, offs);
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < iv->listLen; i++) {
|
||||
iv->addValue(i, payload);
|
||||
}
|
||||
iv->doCalculations();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
void app::showIndex(void) {
|
||||
String html = FPSTR(index_html);
|
||||
|
@ -276,7 +370,6 @@ void app::showSetup(void) {
|
|||
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
|
||||
mEep->read(ADDR_INV_ADDR + (i * 8), &invSerial);
|
||||
mEep->read(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), invName, MAX_NAME_LENGTH);
|
||||
mEep->read(ADDR_INV_TYPE + i, &invType);
|
||||
inv += "<p class=\"subdes\">Inverter "+ String(i) + "</p>";
|
||||
|
||||
inv += "<label for=\"inv" + String(i) + "Addr\">Address</label>";
|
||||
|
@ -289,16 +382,6 @@ void app::showSetup(void) {
|
|||
inv += "<input type=\"text\" class=\"text\" name=\"inv" + String(i) + "Name\" value=\"";
|
||||
inv += String(invName);
|
||||
inv += "\"/ maxlength=\"" + String(MAX_NAME_LENGTH) + "\">";
|
||||
|
||||
inv += "<label for=\"inv" + String(i) + "Type\">Type</label>";
|
||||
inv += "<select name=\"inv" + String(i) + "Type\">";
|
||||
for(uint8_t t = 0; t < NUM_INVERTER_TYPES; t++) {
|
||||
inv += "<option value=\"" + String(t) + "\"";
|
||||
if(invType == t)
|
||||
inv += " selected";
|
||||
inv += ">" + String(invTypes[t]) + "</option>";
|
||||
}
|
||||
inv += "</select>";
|
||||
}
|
||||
html.replace("{INVERTERS}", String(inv));
|
||||
|
||||
|
@ -371,7 +454,7 @@ void app::showSetup(void) {
|
|||
html.replace("{MQTT_PORT}", "1883");
|
||||
html.replace("{MQTT_USER}", "");
|
||||
html.replace("{MQTT_PWD}", "");
|
||||
html.replace("{MQTT_TOPIC}", "/inverter");
|
||||
html.replace("{MQTT_TOPIC}", "inverter");
|
||||
html.replace("{MQTT_INTVL}", "10");
|
||||
|
||||
html.replace("{SER_INTVL}", "10");
|
||||
|
@ -396,12 +479,7 @@ void app::showErase() {
|
|||
|
||||
//-----------------------------------------------------------------------------
|
||||
void app::showStatistics(void) {
|
||||
String content = "Packets:\n";
|
||||
for(uint8_t i = 0; i < DBG_CMD_LIST_LEN; i ++) {
|
||||
content += String("0x") + String(dbgCmds[i], HEX) + String(": ") + String(mPacketIds[i]) + String("\n");
|
||||
}
|
||||
content += String("other: ") + String(mPacketIds[DBG_CMD_LIST_LEN]) + String("\n\n");
|
||||
|
||||
String content = "Failed Payload: " + String(mRxFailed) + "\n";
|
||||
content += "Send Cnt: " + String(mSys->Radio.mSendCnt) + String("\n\n");
|
||||
|
||||
if(!mSys->Radio.isChipConnected())
|
||||
|
@ -440,10 +518,10 @@ void app::showLiveData(void) {
|
|||
#ifdef LIVEDATA_VISUALIZED
|
||||
uint8_t modNum, pos;
|
||||
switch(iv->type) {
|
||||
default: modNum = 1; break;
|
||||
case INV_TYPE_HM600:
|
||||
case INV_TYPE_HM800: modNum = 2; break;
|
||||
case INV_TYPE_HM1200: modNum = 4; break;
|
||||
default:
|
||||
case INV_TYPE_1CH: modNum = 1; break;
|
||||
case INV_TYPE_2CH: modNum = 2; break;
|
||||
case INV_TYPE_4CH: modNum = 4; break;
|
||||
}
|
||||
|
||||
modHtml += "<div class=\"iv\">";
|
||||
|
@ -524,11 +602,6 @@ void app::saveValues(bool webSend = true) {
|
|||
// name
|
||||
mWeb->arg("inv" + String(i) + "Name").toCharArray(buf, 20);
|
||||
mEep->write(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), buf, MAX_NAME_LENGTH);
|
||||
|
||||
// type
|
||||
mWeb->arg("inv" + String(i) + "Type").toCharArray(buf, 20);
|
||||
uint8_t type = atoi(buf);
|
||||
mEep->write(ADDR_INV_TYPE + i, type);
|
||||
}
|
||||
|
||||
interval = mWeb->arg("invInterval").toInt();
|
||||
|
@ -578,15 +651,18 @@ void app::saveValues(bool webSend = true) {
|
|||
mEep->write(ADDR_SER_INTERVAL, interval);
|
||||
tmp = (mWeb->arg("serEn") == "on");
|
||||
mEep->write(ADDR_SER_ENABLE, (uint8_t)((tmp) ? 0x01 : 0x00));
|
||||
tmp = (mWeb->arg("serDbg") == "on");
|
||||
mEep->write(ADDR_SER_DEBUG, (uint8_t)((tmp) ? 0x01 : 0x00));
|
||||
mSerialDebug = (mWeb->arg("serDbg") == "on");
|
||||
mEep->write(ADDR_SER_DEBUG, (uint8_t)((mSerialDebug) ? 0x01 : 0x00));
|
||||
DPRINT("Info: Serial debug is ");
|
||||
if(mSerialDebug) DPRINTLN("on"); else DPRINTLN("off");
|
||||
mSys->Radio.mSerialDebug = mSerialDebug;
|
||||
|
||||
updateCrc();
|
||||
if((mWeb->arg("reboot") == "on"))
|
||||
showReboot();
|
||||
else {
|
||||
mShowRebootRequest = true;
|
||||
mWeb->send(200, "text/html", "<!doctype html><html><head><title>Setup saved</title><meta http-equiv=\"refresh\" content=\"3; URL=/setup\"></head><body>"
|
||||
mWeb->send(200, "text/html", "<!doctype html><html><head><title>Setup saved</title><meta http-equiv=\"refresh\" content=\"1; URL=/setup\"></head><body>"
|
||||
"<p>saved</p></body></html>");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,12 +19,20 @@ typedef HmSystem<RadioType, BufferType, MAX_NUM_INVERTERS, InverterType> HmSyste
|
|||
const char* const wemosPins[] = {"D3 (GPIO0)", "TX (GPIO1)", "D4 (GPIO2)", "RX (GPIO3)",
|
||||
"D2 (GPIO4)", "D1 (GPIO5)", "GPIO6", "GPIO7", "GPIO8",
|
||||
"GPIO9", "GPIO10", "GPIO11", "D6 (GPIO12)", "D7 (GPIO13)",
|
||||
"D5 (GPIO14)", "D8 (GPIO15)", "D0 (GPIO16)"};
|
||||
"D5 (GPIO14)", "D8 (GPIO15)", "D0 (GPIO16 - no IRQ!)"};
|
||||
const char* const pinNames[] = {"CS", "CE", "IRQ"};
|
||||
const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq"};
|
||||
|
||||
const uint8_t dbgCmds[] = {0x01, 0x02, 0x03, 0x81, 0x82, 0x83, 0x84};
|
||||
#define DBG_CMD_LIST_LEN 7
|
||||
|
||||
typedef struct {
|
||||
uint8_t invId;
|
||||
uint32_t ts;
|
||||
uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE];
|
||||
uint8_t len[MAX_PAYLOAD_ENTRIES];
|
||||
bool complete;
|
||||
uint8_t maxPackId;
|
||||
} invPayload_t;
|
||||
|
||||
|
||||
class app : public Main {
|
||||
public:
|
||||
|
@ -40,6 +48,9 @@ class app : public Main {
|
|||
}
|
||||
|
||||
private:
|
||||
bool buildPayload(uint8_t id);
|
||||
void processPayload(bool retransmit);
|
||||
|
||||
void showIndex(void);
|
||||
void showSetup(void);
|
||||
void showSave(void);
|
||||
|
@ -73,8 +84,8 @@ class app : public Main {
|
|||
uint16_t mSendTicker;
|
||||
uint16_t mSendInterval;
|
||||
|
||||
uint32_t mPacketIds[DBG_CMD_LIST_LEN+1];
|
||||
uint32_t mRecCnt;
|
||||
invPayload_t mPayload[MAX_NUM_INVERTERS];
|
||||
uint32_t mRxFailed;
|
||||
|
||||
// timer
|
||||
uint32_t mTicker;
|
||||
|
|
|
@ -38,6 +38,9 @@
|
|||
// maximum buffer length of packet received / sent to RF24 module
|
||||
#define MAX_RF_PAYLOAD_SIZE 32
|
||||
|
||||
// maximum total payload size
|
||||
#define MAX_PAYLOAD_ENTRIES 4
|
||||
|
||||
// changes the style of "/setup" page, visualized = nicer
|
||||
#define LIVEDATA_VISUALIZED
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ uint8_t crc8(uint8_t buf[], uint8_t len) {
|
|||
return crc;
|
||||
}
|
||||
|
||||
uint16_t crc16(uint8_t buf[], uint8_t len) {
|
||||
uint16_t crc = 0xffff;
|
||||
uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start) {
|
||||
uint16_t crc = start;
|
||||
uint8_t shift = 0;
|
||||
|
||||
for(uint8_t i = 0; i < len; i ++) {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
#define CRC16_NRF24_POLYNOM 0x1021
|
||||
|
||||
uint8_t crc8(uint8_t buf[], uint8_t len);
|
||||
uint16_t crc16(uint8_t buf[], uint8_t len);
|
||||
uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start = 0xffff);
|
||||
uint16_t crc16nrf24(uint8_t buf[], uint16_t lenBits, uint16_t startBit = 0, uint16_t crcIn = 0xffff);
|
||||
|
||||
#endif /*__CRC_H__*/
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
// VERSION
|
||||
//-------------------------------------
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 3
|
||||
#define VERSION_PATCH 9
|
||||
#define VERSION_MINOR 4
|
||||
#define VERSION_PATCH 3
|
||||
|
||||
|
||||
//-------------------------------------
|
||||
|
@ -68,7 +68,7 @@ typedef struct {
|
|||
|
||||
#define ADDR_INV_ADDR ADDR_RF24_AMP_PWR + RF24_AMP_PWR_LEN
|
||||
#define ADDR_INV_NAME ADDR_INV_ADDR + INV_ADDR_LEN
|
||||
#define ADDR_INV_TYPE ADDR_INV_NAME + INV_NAME_LEN
|
||||
#define ADDR_INV_TYPE ADDR_INV_NAME + INV_NAME_LEN // obsolete
|
||||
#define ADDR_INV_INTERVAL ADDR_INV_TYPE + INV_TYPE_LEN
|
||||
|
||||
#define ADDR_MQTT_ADDR ADDR_INV_INTERVAL + INV_INTERVAL_LEN
|
||||
|
|
|
@ -25,26 +25,22 @@ const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "
|
|||
|
||||
// indices to calculation functions, defined in hmInverter.h
|
||||
enum {CALC_YT_CH0 = 0, CALC_YD_CH0, CALC_UDC_CH};
|
||||
enum {CMD_CALC = 0xffff};
|
||||
|
||||
|
||||
// CH0 is default channel (freq, ac, temp)
|
||||
enum {CH0 = 0, CH1, CH2, CH3, CH4};
|
||||
// received command ids, special command CMDFF for calculations
|
||||
enum {CMD01 = 0x01, CMD02, CMD03, CMD82 = 0x82, CMD83, CMD84, CMDFF=0xff};
|
||||
|
||||
enum {INV_TYPE_HM600 = 0, INV_TYPE_HM1200, INV_TYPE_HM400, INV_TYPE_HM800};
|
||||
const char* const invTypes[] = {"HM600", "HM1200 / HM1500", "HM400", "HM800"};
|
||||
#define NUM_INVERTER_TYPES 4
|
||||
enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH};
|
||||
|
||||
|
||||
typedef struct {
|
||||
uint8_t fieldId; // field id
|
||||
uint8_t unitId; // uint id
|
||||
uint8_t ch; // channel 0 - 3
|
||||
uint8_t cmdId; // received command id
|
||||
uint8_t ch; // channel 0 - 4
|
||||
uint8_t start; // pos of first byte in buffer
|
||||
uint8_t num; // number of bytes in buffer
|
||||
uint16_t div; // divisor
|
||||
uint16_t div; // divisor / calc command
|
||||
} byteAssign_t;
|
||||
|
||||
|
||||
|
@ -54,106 +50,90 @@ typedef struct {
|
|||
* */
|
||||
|
||||
//-------------------------------------
|
||||
// HM400 HM350?, HM300?
|
||||
// HM300, HM350, HM400
|
||||
//-------------------------------------
|
||||
const byteAssign_t hm400assignment[] = {
|
||||
{ FLD_UDC, UNIT_V, CH1, CMD01, 3, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH1, CMD01, 5, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH1, CMD01, 7, 2, 10 },
|
||||
{ FLD_YT, UNIT_KWH, CH1, CMD01, 9, 4, 1000 },
|
||||
{ FLD_YD, UNIT_WH, CH1, CMD01, 13, 2, 1 },
|
||||
{ FLD_UAC, UNIT_V, CH0, CMD01, 15, 2, 10 },
|
||||
{ FLD_F, UNIT_HZ, CH0, CMD82, 1, 2, 100 },
|
||||
{ FLD_PAC, UNIT_W, CH0, CMD82, 3, 2, 10 },
|
||||
{ FLD_IAC, UNIT_A, CH0, CMD82, 7, 2, 100 },
|
||||
{ FLD_T, UNIT_C, CH0, CMD82, 11, 2, 10 }
|
||||
const byteAssign_t hm1chAssignment[] = {
|
||||
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH1, 4, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH1, 6, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH1, 12, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH1, 8, 4, 1000 },
|
||||
|
||||
{ FLD_UAC, UNIT_V, CH0, 14, 2, 10 },
|
||||
{ FLD_IAC, UNIT_A, CH0, 22, 2, 100 },
|
||||
{ FLD_PAC, UNIT_W, CH0, 18, 2, 10 },
|
||||
{ FLD_F, UNIT_HZ, CH0, 16, 2, 100 },
|
||||
{ FLD_T, UNIT_C, CH0, 26, 2, 10 }
|
||||
};
|
||||
#define HM400_LIST_LEN (sizeof(hm400assignment) / sizeof(byteAssign_t))
|
||||
#define HM1CH_LIST_LEN (sizeof(hm1chAssignment) / sizeof(byteAssign_t))
|
||||
|
||||
|
||||
//-------------------------------------
|
||||
// HM600, HM700
|
||||
// HM600, HM700, HM800
|
||||
//-------------------------------------
|
||||
const byteAssign_t hm600assignment[] = {
|
||||
{ FLD_UDC, UNIT_V, CH1, CMD01, 3, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH1, CMD01, 5, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH1, CMD01, 7, 2, 10 },
|
||||
{ FLD_UDC, UNIT_V, CH2, CMD01, 9, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH2, CMD01, 11, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH2, CMD01, 13, 2, 10 },
|
||||
{ FLD_YW, UNIT_WH, CH0, CMD02, 1, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH0, CMD02, 3, 4, 1000 },
|
||||
{ FLD_YD, UNIT_WH, CH1, CMD02, 7, 2, 1 },
|
||||
{ FLD_YD, UNIT_WH, CH2, CMD02, 9, 2, 1 },
|
||||
{ FLD_UAC, UNIT_V, CH0, CMD02, 11, 2, 10 },
|
||||
{ FLD_F, UNIT_HZ, CH0, CMD02, 13, 2, 100 },
|
||||
{ FLD_PAC, UNIT_W, CH0, CMD02, 15, 2, 10 },
|
||||
{ FLD_IAC, UNIT_A, CH0, CMD83, 3, 2, 100 },
|
||||
{ FLD_T, UNIT_C, CH0, CMD83, 7, 2, 10 }
|
||||
const byteAssign_t hm2chAssignment[] = {
|
||||
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH1, 4, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH1, 6, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH1, 22, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH1, 14, 4, 1000 },
|
||||
|
||||
{ FLD_UDC, UNIT_V, CH2, 8, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH2, 10, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH2, 12, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH2, 24, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH2, 18, 4, 1000 },
|
||||
|
||||
{ FLD_UAC, UNIT_V, CH0, 26, 2, 10 },
|
||||
{ FLD_IAC, UNIT_A, CH0, 34, 2, 10 },
|
||||
{ FLD_PAC, UNIT_W, CH0, 30, 2, 10 },
|
||||
{ FLD_F, UNIT_HZ, CH0, 28, 2, 100 },
|
||||
{ FLD_T, UNIT_C, CH0, 38, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
|
||||
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }
|
||||
|
||||
};
|
||||
#define HM600_LIST_LEN (sizeof(hm600assignment) / sizeof(byteAssign_t))
|
||||
|
||||
|
||||
//-------------------------------------
|
||||
// HM800
|
||||
//-------------------------------------
|
||||
const byteAssign_t hm800assignment[] = {
|
||||
|
||||
{ FLD_UDC, UNIT_V, CH1, CMD01, 3, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH1, CMD01, 5, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH1, CMD01, 7, 2, 10 },
|
||||
{ FLD_UDC, UNIT_V, CH2, CMD01, 9, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH2, CMD01, 11, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH2, CMD01, 13, 2, 10 },
|
||||
{ FLD_YW, UNIT_WH, CH0, CMD02, 1, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH0, CMD02, 3, 4, 1000 },
|
||||
{ FLD_YD, UNIT_WH, CH1, CMD02, 7, 2, 1 },
|
||||
{ FLD_YD, UNIT_WH, CH2, CMD02, 9, 2, 1 },
|
||||
{ FLD_UAC, UNIT_V, CH0, CMD02, 11, 2, 10 },
|
||||
{ FLD_F, UNIT_HZ, CH0, CMD02, 13, 2, 100 },
|
||||
{ FLD_PAC, UNIT_W, CH0, CMD02, 15, 2, 10 },
|
||||
{ FLD_IAC, UNIT_A, CH0, CMD83, 3, 2, 100 },
|
||||
{ FLD_T, UNIT_C, CH0, CMD83, 7, 2, 10 }
|
||||
};
|
||||
#define HM800_LIST_LEN (sizeof(hm800assignment) / sizeof(byteAssign_t))
|
||||
#define HM2CH_LIST_LEN (sizeof(hm2chAssignment) / sizeof(byteAssign_t))
|
||||
|
||||
|
||||
//-------------------------------------
|
||||
// HM1200, HM1500
|
||||
//-------------------------------------
|
||||
const byteAssign_t hm1200assignment[] = {
|
||||
{ FLD_UDC, UNIT_V, CH1, CMD01, 3, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH1, CMD01, 5, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH1, CMD01, 9, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH1, CMD02, 5, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH1, CMD01, 13, 4, 1000 },
|
||||
{ FLD_UDC, UNIT_V, CH3, CMD02, 9, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH2, CMD01, 7, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH2, CMD01, 11, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH2, CMD02, 7, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH2, CMD02, 1, 4, 1000 },
|
||||
{ FLD_IDC, UNIT_A, CH3, CMD02, 11, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH3, CMD02, 15, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH3, CMD03, 11, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH3, CMD03, 3, 4, 1000 },
|
||||
{ FLD_IDC, UNIT_A, CH4, CMD02, 13, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH4, CMD03, 1, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH4, CMD03, 13, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH4, CMD03, 7, 4, 1000 },
|
||||
{ FLD_UAC, UNIT_V, CH0, CMD03, 15, 2, 10 },
|
||||
{ FLD_IAC, UNIT_A, CH0, CMD84, 7, 2, 100 },
|
||||
{ FLD_PAC, UNIT_W, CH0, CMD84, 3, 2, 10 },
|
||||
{ FLD_F, UNIT_HZ, CH0, CMD84, 1, 2, 100 },
|
||||
{ FLD_PCT, UNIT_PCT, CH0, CMD84, 9, 2, 10 },
|
||||
{ FLD_T, UNIT_C, CH0, CMD84, 11, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH0, CMDFF, CALC_YD_CH0, 0, 0 },
|
||||
{ FLD_YT, UNIT_KWH, CH0, CMDFF, CALC_YT_CH0, 0, 0 },
|
||||
{ FLD_UDC, UNIT_V, CH2, CMDFF, CALC_UDC_CH, CH1, 0 },
|
||||
{ FLD_UDC, UNIT_V, CH4, CMDFF, CALC_UDC_CH, CH3, 0 }
|
||||
const byteAssign_t hm4chAssignment[] = {
|
||||
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH1, 4, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH1, 8, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH1, 20, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH1, 12, 4, 1000 },
|
||||
|
||||
{ FLD_UDC, UNIT_V, CH2, CALC_UDC_CH, CH1, CMD_CALC },
|
||||
{ FLD_IDC, UNIT_A, CH2, 6, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH2, 10, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH2, 22, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH2, 16, 4, 1000 },
|
||||
|
||||
{ FLD_UDC, UNIT_V, CH3, 24, 2, 10 },
|
||||
{ FLD_IDC, UNIT_A, CH3, 26, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH3, 30, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH3, 42, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH3, 34, 4, 1000 },
|
||||
|
||||
{ FLD_UDC, UNIT_V, CH4, CALC_UDC_CH, CH3, CMD_CALC },
|
||||
{ FLD_IDC, UNIT_A, CH4, 28, 2, 100 },
|
||||
{ FLD_PDC, UNIT_W, CH4, 32, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH4, 44, 2, 1 },
|
||||
{ FLD_YT, UNIT_KWH, CH4, 38, 4, 1000 },
|
||||
|
||||
{ FLD_UAC, UNIT_V, CH0, 46, 2, 10 },
|
||||
{ FLD_IAC, UNIT_A, CH0, 54, 2, 100 },
|
||||
{ FLD_PAC, UNIT_W, CH0, 50, 2, 10 },
|
||||
{ FLD_F, UNIT_HZ, CH0, 48, 2, 100 },
|
||||
{ FLD_PCT, UNIT_PCT, CH0, 56, 2, 10 },
|
||||
{ FLD_T, UNIT_C, CH0, 58, 2, 10 },
|
||||
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
|
||||
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }
|
||||
};
|
||||
#define HM1200_LIST_LEN (sizeof(hm1200assignment) / sizeof(byteAssign_t))
|
||||
|
||||
|
||||
#define HM4CH_LIST_LEN (sizeof(hm4chAssignment) / sizeof(byteAssign_t))
|
||||
|
||||
|
||||
#endif /*__HM_DEFINES_H__*/
|
||||
|
|
|
@ -95,22 +95,20 @@ class Inverter {
|
|||
return assign[pos].ch;
|
||||
}
|
||||
|
||||
uint8_t getCmdId(uint8_t pos) {
|
||||
return assign[pos].cmdId;
|
||||
}
|
||||
|
||||
void addValue(uint8_t pos, uint8_t buf[]) {
|
||||
uint8_t ptr = assign[pos].start;
|
||||
uint8_t end = ptr + assign[pos].num;
|
||||
uint16_t div = assign[pos].div;
|
||||
|
||||
uint32_t val = 0;
|
||||
do {
|
||||
val <<= 8;
|
||||
val |= buf[ptr];
|
||||
} while(++ptr != end);
|
||||
if(CMD_CALC != div) {
|
||||
uint32_t val = 0;
|
||||
do {
|
||||
val <<= 8;
|
||||
val |= buf[ptr];
|
||||
} while(++ptr != end);
|
||||
|
||||
record[pos] = (RECORDTYPE)(val) / (RECORDTYPE)(div);
|
||||
record[pos] = (RECORDTYPE)(val) / (RECORDTYPE)(div);
|
||||
}
|
||||
}
|
||||
|
||||
RECORDTYPE getValue(uint8_t pos) {
|
||||
|
@ -119,7 +117,7 @@ class Inverter {
|
|||
|
||||
void doCalculations(void) {
|
||||
for(uint8_t i = 0; i < listLen; i++) {
|
||||
if(CMDFF == assign[i].cmdId) {
|
||||
if(CMD_CALC == assign[i].div) {
|
||||
record[i] = calcFunctions<RECORDTYPE>[assign[i].start].func(this, assign[i].num);
|
||||
}
|
||||
}
|
||||
|
@ -136,24 +134,19 @@ class Inverter {
|
|||
}
|
||||
|
||||
void getAssignment(void) {
|
||||
if(INV_TYPE_HM400 == type) {
|
||||
listLen = (uint8_t)(HM400_LIST_LEN);
|
||||
assign = (byteAssign_t*)hm400assignment;
|
||||
if(INV_TYPE_1CH == type) {
|
||||
listLen = (uint8_t)(HM1CH_LIST_LEN);
|
||||
assign = (byteAssign_t*)hm1chAssignment;
|
||||
channels = 1;
|
||||
}
|
||||
else if(INV_TYPE_HM600 == type) {
|
||||
listLen = (uint8_t)(HM600_LIST_LEN);
|
||||
assign = (byteAssign_t*)hm600assignment;
|
||||
else if(INV_TYPE_2CH == type) {
|
||||
listLen = (uint8_t)(HM2CH_LIST_LEN);
|
||||
assign = (byteAssign_t*)hm2chAssignment;
|
||||
channels = 2;
|
||||
}
|
||||
else if(INV_TYPE_HM800 == type) {
|
||||
listLen = (uint8_t)(HM800_LIST_LEN);
|
||||
assign = (byteAssign_t*)hm800assignment;
|
||||
channels = 2;
|
||||
}
|
||||
else if(INV_TYPE_HM1200 == type) {
|
||||
listLen = (uint8_t)(HM1200_LIST_LEN);
|
||||
assign = (byteAssign_t*)hm1200assignment;
|
||||
else if(INV_TYPE_4CH == type) {
|
||||
listLen = (uint8_t)(HM4CH_LIST_LEN);
|
||||
assign = (byteAssign_t*)hm4chAssignment;
|
||||
channels = 4;
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -58,14 +58,14 @@ class HmRadio {
|
|||
mRxChIdx = 0;
|
||||
mRxLoopCnt = RX_LOOP_CNT;
|
||||
|
||||
//calcDtuCrc();
|
||||
|
||||
pinCs = CS_PIN;
|
||||
pinCe = CE_PIN;
|
||||
pinIrq = IRQ_PIN;
|
||||
|
||||
AmplifierPower = 1;
|
||||
mSendCnt = 0;
|
||||
|
||||
mSerialDebug = false;
|
||||
}
|
||||
~HmRadio() {}
|
||||
|
||||
|
@ -111,7 +111,7 @@ class HmRadio {
|
|||
if(!mBufCtrl->full()) {
|
||||
p = mBufCtrl->getFront();
|
||||
memset(p->packet, 0xcc, MAX_RF_PAYLOAD_SIZE);
|
||||
p->rxCh = mRxChIdx;
|
||||
p->rxCh = mRxChLst[mRxChIdx];
|
||||
len = mNrf24.getPayloadSize();
|
||||
if(len > MAX_RF_PAYLOAD_SIZE)
|
||||
len = MAX_RF_PAYLOAD_SIZE;
|
||||
|
@ -156,19 +156,19 @@ class HmRadio {
|
|||
sendPacket(invId, mTxBuf, 27, true);
|
||||
}
|
||||
|
||||
void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t cmd, bool calcCrc = true) {
|
||||
void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool calcCrc = true) {
|
||||
memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE);
|
||||
mTxBuf[0] = mid; // message id
|
||||
CP_U32_BigEndian(&mTxBuf[1], (invId >> 8));
|
||||
CP_U32_BigEndian(&mTxBuf[5], (DTU_ID >> 8));
|
||||
mTxBuf[9] = cmd;
|
||||
mTxBuf[9] = pid;
|
||||
if(calcCrc) {
|
||||
mTxBuf[10] = crc8(mTxBuf, 10);
|
||||
sendPacket(invId, mTxBuf, 11, false);
|
||||
}
|
||||
}
|
||||
|
||||
bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t *rptCnt, uint8_t rxCh) {
|
||||
bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t rxCh) {
|
||||
*len = (buf[0] >> 2);
|
||||
if(*len > (MAX_RF_PAYLOAD_SIZE - 2))
|
||||
*len = MAX_RF_PAYLOAD_SIZE - 2;
|
||||
|
@ -179,17 +179,10 @@ class HmRadio {
|
|||
uint8_t crc = crc8(buf, *len-1);
|
||||
bool valid = (crc == buf[*len-1]);
|
||||
|
||||
if(valid) {
|
||||
if(mLastCrc == crc)
|
||||
*rptCnt = (++mRptCnt);
|
||||
else {
|
||||
mRptCnt = 0;
|
||||
*rptCnt = 0;
|
||||
mLastCrc = crc;
|
||||
}
|
||||
mRxStat[(buf[9] & 0x7F)-1]++;
|
||||
mRxChStat[(buf[9] & 0x7F)-1][rxCh & 0x7]++;
|
||||
}
|
||||
//if(valid) {
|
||||
//mRxStat[(buf[9] & 0x7F)-1]++;
|
||||
//mRxChStat[(buf[9] & 0x7F)-1][rxCh & 0x7]++;
|
||||
//}
|
||||
/*else {
|
||||
DPRINT("CRC wrong: ");
|
||||
DHEX(crc);
|
||||
|
@ -215,7 +208,8 @@ class HmRadio {
|
|||
}
|
||||
|
||||
void dumpBuf(const char *info, uint8_t buf[], uint8_t len) {
|
||||
DPRINT(String(info));
|
||||
if(NULL != info)
|
||||
DPRINT(String(info));
|
||||
for(uint8_t i = 0; i < len; i++) {
|
||||
DHEX(buf[i]);
|
||||
DPRINT(" ");
|
||||
|
@ -234,16 +228,22 @@ class HmRadio {
|
|||
uint8_t AmplifierPower;
|
||||
uint32_t mSendCnt;
|
||||
|
||||
bool mSerialDebug;
|
||||
|
||||
private:
|
||||
void sendPacket(uint64_t invId, uint8_t buf[], uint8_t len, bool clear=false) {
|
||||
//DPRINTLN("sent packet: #" + String(mSendCnt));
|
||||
//dumpBuf("SEN ", buf, len);
|
||||
if(mSerialDebug) {
|
||||
DPRINT("Transmit " + String(len) + " | ");
|
||||
dumpBuf(NULL, buf, len);
|
||||
}
|
||||
|
||||
DISABLE_IRQ;
|
||||
mNrf24.stopListening();
|
||||
|
||||
if(clear) {
|
||||
uint8_t cnt = 4;
|
||||
/*uint8_t cnt = 4;
|
||||
for(uint8_t i = 0; i < 4; i ++) {
|
||||
DPRINT(String(mRxStat[i]) + " (");
|
||||
for(uint8_t j = 0; j < 4; j++) {
|
||||
|
@ -258,7 +258,7 @@ class HmRadio {
|
|||
else
|
||||
DPRINTLN(" -> missing: " + String(cnt));
|
||||
memset(mRxStat, 0, 4);
|
||||
memset(mRxChStat, 0, 4*8);
|
||||
memset(mRxChStat, 0, 4*8);*/
|
||||
mRxLoopCnt = RX_LOOP_CNT;
|
||||
}
|
||||
|
||||
|
@ -295,34 +295,19 @@ class HmRadio {
|
|||
return mRxChLst[mRxChIdx];
|
||||
}
|
||||
|
||||
/*void calcDtuCrc(void) {
|
||||
uint64_t addr = DTU_RADIO_ID;
|
||||
uint8_t tmp[5];
|
||||
for(int8_t i = 4; i >= 0; i--) {
|
||||
tmp[i] = addr;
|
||||
addr >>= 8;
|
||||
}
|
||||
mDtuIdCrc = crc16nrf24(tmp, BIT_CNT(5));
|
||||
}*/
|
||||
|
||||
uint8_t mTxCh;
|
||||
uint8_t mTxChLst[1];
|
||||
//uint8_t mTxChIdx;
|
||||
|
||||
uint8_t mRxChLst[4];
|
||||
uint8_t mRxChIdx;
|
||||
uint8_t mRxStat[4];
|
||||
uint8_t mRxChStat[4][8];
|
||||
//uint8_t mRxStat[4];
|
||||
//uint8_t mRxChStat[4][8];
|
||||
uint16_t mRxLoopCnt;
|
||||
|
||||
//uint16_t mDtuIdCrc;
|
||||
uint16_t mLastCrc;
|
||||
uint8_t mRptCnt;
|
||||
|
||||
RF24 mNrf24;
|
||||
BUFFER *mBufCtrl;
|
||||
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE];
|
||||
|
||||
};
|
||||
|
||||
#endif /*__RADIO_H__*/
|
||||
|
|
|
@ -27,7 +27,7 @@ class HmSystem {
|
|||
Radio.setup(&BufCtrl);
|
||||
}
|
||||
|
||||
INVERTERTYPE *addInverter(const char *name, uint64_t serial, uint8_t type) {
|
||||
INVERTERTYPE *addInverter(const char *name, uint64_t serial) {
|
||||
if(MAX_INVERTER <= mNumInv) {
|
||||
DPRINT("max number of inverters reached!");
|
||||
return NULL;
|
||||
|
@ -35,7 +35,19 @@ class HmSystem {
|
|||
INVERTERTYPE *p = &mInverter[mNumInv];
|
||||
p->id = mNumInv;
|
||||
p->serial.u64 = serial;
|
||||
p->type = type;
|
||||
DPRINT("SERIAL: " + String(p->serial.b[5], HEX));
|
||||
DPRINTLN(" " + String(p->serial.b[4], HEX));
|
||||
if(p->serial.b[5] == 0x11) {
|
||||
switch(p->serial.b[4]) {
|
||||
case 0x21: p->type = INV_TYPE_1CH; break;
|
||||
case 0x41: p->type = INV_TYPE_2CH; break;
|
||||
case 0x61: p->type = INV_TYPE_4CH; break;
|
||||
default: DPRINTLN("unknown inverter type: 11" + String(p->serial.b[4], HEX)); break;
|
||||
}
|
||||
}
|
||||
else
|
||||
DPRINTLN("inverter type can't be detected!");
|
||||
|
||||
p->init();
|
||||
uint8_t len = (uint8_t)strlen(name);
|
||||
strncpy(p->name, name, (len > MAX_NAME_LENGTH) ? MAX_NAME_LENGTH : len);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#ifndef __STYLE_CSS_H__
|
||||
#define __STYLE_CSS_H__
|
||||
const char style_css[] PROGMEM = "h1 {margin:0;padding:20pt;font-size:22pt;color:#fff;background-color:#006ec0;display:block;text-transform:uppercase;}html, body {font-family:Arial;margin:0;padding:0;}p {text-align:justify;font-size:13pt;}.des {margin-top:35px;font-size:13pt;color:#006ec0;}.subdes {font-size:12pt;color:#006ec0;margin-left:7px;}a:link, a:visited {text-decoration:none;font-size:13pt;color:#006ec0;}a:hover, a:focus {color:#f00;}a.erase {background-color:#006ec0;color:#fff;padding:7px;display:inline-block;margin-top:30px;float:right;}#content {padding:15px 15px 60px 15px;}#footer {position:fixed;bottom:0px;height:45px;background-color:#006ec0;width:100%;border-top:5px solid #fff;}#footer p, #footer a {color:#fff;padding:0 7px 0 7px;font-size:10pt !important;}div.content {background-color:#fff;padding-bottom:65px;overflow:auto;}input, select {padding:7px;font-size:13pt;}input.text, select {width:70%;box-sizing:border-box;margin-bottom:10px;border:1px solid #ccc;}input.btn {background-color:#006ec0;color:#fff;border:0px;float:right;margin:10px 0 30px;text-transform:uppercase;}input.cb {margin-bottom:20px;}label {width:20%;display:inline-block;font-size:12pt;padding-right:10px;margin-left:10px;}.left {float:left;}.right {float:right;}div.ch-iv {width:100%;background-color:#32b004;display:inline-block;margin-bottom:20px;padding-bottom:20px;overflow:auto;}div.ch {width:250px;min-height:420px;background-color:#006ec0;display:inline-block;margin-right:20px;margin-bottom:20px;overflow:auto;padding-bottom:20px;}div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head {color:#fff;display:block;width:100%;text-align:center;}.subgrp {float:left;width:250px;}div.ch .unit, div.ch-iv .unit {font-size:19px;margin-left:10px;}div.ch .value, div.ch-iv .value {margin-top:20px;font-size:30px;}div.ch .info, div.ch-iv .info {margin-top:3px;font-size:10px;}div.ch .head {background-color:#003c80;padding:10px 0 10px 0;}div.ch-iv .head {background-color:#1c6800;padding:10px 0 10px 0;}div.iv {max-width:1060px;}div.ch:last-child {margin-right:0px !important;}#note {margin:50px 10px 10px 10px;padding-top:10px;width:100%;border-top:1px solid #bbb;}";
|
||||
const char style_css[] PROGMEM = "h1 {margin:0;padding:20pt;font-size:22pt;color:#fff;background-color:#006ec0;display:block;text-transform:uppercase;}html, body {font-family:Arial;margin:0;padding:0;}p {text-align:justify;font-size:13pt;}.des {margin-top:35px;font-size:13pt;color:#006ec0;}.subdes {font-size:12pt;color:#006ec0;margin-left:7px;}a:link, a:visited {text-decoration:none;font-size:13pt;color:#006ec0;}a:hover, a:focus {color:#f00;}a.erase {background-color:#006ec0;color:#fff;padding:7px;display:inline-block;margin-top:30px;float:right;}#content {padding:15px 15px 60px 15px;}#footer {position:fixed;bottom:0px;height:45px;background-color:#006ec0;width:100%;border-top:5px solid #fff;}#footer p, #footer a {color:#fff;padding:0 7px 0 7px;font-size:10pt !important;}div.content {background-color:#fff;padding-bottom:65px;overflow:auto;}input, select {padding:7px;font-size:13pt;}input.text, select {width:70%;box-sizing:border-box;margin-bottom:10px;border:1px solid #ccc;}input.btn {background-color:#006ec0;color:#fff;border:0px;float:right;margin:10px 0 30px;text-transform:uppercase;}input.cb {margin-bottom:20px;}label {width:20%;display:inline-block;font-size:12pt;padding-right:10px;margin-left:10px;}.left {float:left;}.right {float:right;}div.ch-iv {width:100%;background-color:#32b004;display:inline-block;margin-bottom:20px;padding-bottom:20px;overflow:auto;}div.ch {width:250px;min-height:420px;background-color:#006ec0;display:inline-block;margin-right:20px;margin-bottom:20px;overflow:auto;padding-bottom:20px;}div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head {color:#fff;display:block;width:100%;text-align:center;}.subgrp {float:left;width:250px;}div.ch .unit, div.ch-iv .unit {font-size:19px;margin-left:10px;}div.ch .value, div.ch-iv .value {margin-top:20px;font-size:30px;}div.ch .info, div.ch-iv .info {margin-top:3px;font-size:10px;}div.ch .head {background-color:#003c80;padding:10px 0 10px 0;}div.ch-iv .head {background-color:#1c6800;padding:10px 0 10px 0;}div.iv {max-width:1060px;}div.ch:last-child {margin-right:0px !important;}#note {margin:50px 10px 10px 10px;padding-top:10px;width:100%;border-top:1px solid #bbb;}@media(max-width:500px) {div.ch .unit, div.ch-iv .unit {font-size:18px;}div.ch {width:170px;min-height:100px;}.subgrp {width:180px;}}";
|
||||
#endif /*__STYLE_CSS_H__*/
|
||||
|
|
|
@ -188,3 +188,18 @@ div.ch:last-child {
|
|||
width: 100%;
|
||||
border-top: 1px solid #bbb;
|
||||
}
|
||||
|
||||
@media(max-width: 500px) {
|
||||
div.ch .unit, div.ch-iv .unit {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
div.ch {
|
||||
width: 170px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.subgrp {
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,15 @@ ahoy:
|
|||
user: 'username'
|
||||
password: 'password'
|
||||
|
||||
# Influx2 output
|
||||
influxdb:
|
||||
disabled: true
|
||||
url: 'http://influxserver.local:8086'
|
||||
org: 'myorg'
|
||||
token: '<base64-token>'
|
||||
bucket: 'telegraf/autogen'
|
||||
measurement: 'hoymiles'
|
||||
|
||||
dtu:
|
||||
serial: 99978563001
|
||||
|
||||
|
|
|
@ -1,62 +1,65 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Hoymiles micro-inverters python shared code
|
||||
"""
|
||||
|
||||
import struct
|
||||
import crcmod
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
from datetime import datetime
|
||||
import json
|
||||
import crcmod
|
||||
from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
|
||||
from .decoders import *
|
||||
|
||||
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
|
||||
|
||||
|
||||
HOYMILES_TRANSACTION_LOGGING=False
|
||||
HOYMILES_DEBUG_LOGGING=False
|
||||
|
||||
def ser_to_hm_addr(s):
|
||||
def ser_to_hm_addr(inverter_ser):
|
||||
"""
|
||||
Calculate the 4 bytes that the HM devices use in their internal messages to
|
||||
Calculate the 4 bytes that the HM devices use in their internal messages to
|
||||
address each other.
|
||||
|
||||
:param str s: inverter serial
|
||||
:param str inverter_ser: inverter serial
|
||||
:return: inverter address
|
||||
:rtype: bytes
|
||||
"""
|
||||
bcd = int(str(s)[-8:], base=16)
|
||||
bcd = int(str(inverter_ser)[-8:], base=16)
|
||||
return struct.pack('>L', bcd)
|
||||
|
||||
def ser_to_esb_addr(s):
|
||||
def ser_to_esb_addr(inverter_ser):
|
||||
"""
|
||||
Convert a Hoymiles inverter/DTU serial number into its
|
||||
corresponding NRF24 'enhanced shockburst' address byte sequence (5 bytes).
|
||||
|
||||
The NRF library expects these in LSB to MSB order, even though the transceiver
|
||||
itself will then output them in MSB-to-LSB order over the air.
|
||||
|
||||
|
||||
The inverters use a BCD representation of the last 8
|
||||
digits of their serial number, in reverse byte order,
|
||||
digits of their serial number, in reverse byte order,
|
||||
followed by \x01.
|
||||
|
||||
:param str s: inverter serial
|
||||
:param str inverter_ser: inverter serial
|
||||
:return: ESB inverter address
|
||||
:rtype: bytes
|
||||
"""
|
||||
air_order = ser_to_hm_addr(s)[::-1] + b'\x01'
|
||||
air_order = ser_to_hm_addr(inverter_ser)[::-1] + b'\x01'
|
||||
return air_order[::-1]
|
||||
|
||||
def print_addr(a):
|
||||
def print_addr(inverter_ser):
|
||||
"""
|
||||
Debug print addresses
|
||||
|
||||
:param str a: inverter serial
|
||||
:param str inverter_ser: inverter serial
|
||||
"""
|
||||
print(f"ser# {a} ", end='')
|
||||
print(f" -> HM {' '.join([f'{x:02x}' for x in ser_to_hm_addr(a)])}", end='')
|
||||
print(f" -> ESB {' '.join([f'{x:02x}' for x in ser_to_esb_addr(a)])}")
|
||||
|
||||
# time of last transmission - to calculcate response time
|
||||
t_last_tx = 0
|
||||
print(f"ser# {inverter_ser} ", end='')
|
||||
print(f" -> HM {' '.join([f'{byte:02x}' for byte in ser_to_hm_addr(inverter_ser)])}", end='')
|
||||
print(f" -> ESB {' '.join([f'{byte:02x}' for byte in ser_to_esb_addr(inverter_ser)])}")
|
||||
|
||||
class ResponseDecoderFactory:
|
||||
"""
|
||||
|
@ -67,14 +70,19 @@ class ResponseDecoderFactory:
|
|||
:type request: bytes
|
||||
:param inverter_ser: inverter serial
|
||||
:type inverter_ser: str
|
||||
:param time_rx: idatetime when payload was received
|
||||
:type time_rx: datetime
|
||||
"""
|
||||
model = None
|
||||
request = None
|
||||
response = None
|
||||
time_rx = None
|
||||
|
||||
def __init__(self, response, **params):
|
||||
self.response = response
|
||||
|
||||
self.time_rx = params.get('time_rx', datetime.now())
|
||||
|
||||
if 'request' in params:
|
||||
self.request = params['request']
|
||||
elif hasattr(response, 'request'):
|
||||
|
@ -110,16 +118,16 @@ class ResponseDecoderFactory:
|
|||
raise ValueError('Inverter serial while decoding response')
|
||||
|
||||
ser_db = [
|
||||
('HM300', r'^1121........'),
|
||||
('HM600', r'^1141........'),
|
||||
('HM1200', r'^1161........'),
|
||||
('Hm300', r'^1121........'),
|
||||
('Hm600', r'^1141........'),
|
||||
('Hm1200', r'^1161........'),
|
||||
]
|
||||
ser_str = str(self.inverter_ser)
|
||||
|
||||
model = None
|
||||
for m, r in ser_db:
|
||||
if re.match(r, ser_str):
|
||||
model = m
|
||||
for s_model, r_match in ser_db:
|
||||
if re.match(r_match, ser_str):
|
||||
model = s_model
|
||||
break
|
||||
|
||||
if len(model):
|
||||
|
@ -157,14 +165,17 @@ class ResponseDecoder(ResponseDecoderFactory):
|
|||
model = self.inverter_model
|
||||
command = self.request_command
|
||||
|
||||
model_decoders = __import__(f'hoymiles.decoders')
|
||||
if hasattr(model_decoders, f'{model}_Decode{command.upper()}'):
|
||||
device = getattr(model_decoders, f'{model}_Decode{command.upper()}')
|
||||
model_decoders = __import__('hoymiles.decoders')
|
||||
if hasattr(model_decoders, f'{model}Decode{command.upper()}'):
|
||||
device = getattr(model_decoders, f'{model}Decode{command.upper()}')
|
||||
else:
|
||||
if HOYMILES_DEBUG_LOGGING:
|
||||
device = getattr(model_decoders, f'DEBUG_DecodeAny')
|
||||
device = getattr(model_decoders, 'DebugDecodeAny')
|
||||
|
||||
return device(self.response)
|
||||
return device(self.response,
|
||||
time_rx=self.time_rx,
|
||||
inverter_ser=self.inverter_ser
|
||||
)
|
||||
|
||||
class InverterPacketFragment:
|
||||
"""ESB Frame"""
|
||||
|
@ -180,6 +191,8 @@ class InverterPacketFragment:
|
|||
:type ch_rx: int
|
||||
:param ch_tx: channel where request was sent
|
||||
:type ch_tx: int
|
||||
|
||||
:raises BufferError: when data gets lost on SPI bus
|
||||
"""
|
||||
|
||||
if not time_rx:
|
||||
|
@ -247,11 +260,11 @@ class InverterPacketFragment:
|
|||
:return: log line received frame
|
||||
:rtype: str
|
||||
"""
|
||||
dt = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
size = len(self.frame)
|
||||
channel = f' channel {self.ch_rx}' if self.ch_rx else ''
|
||||
raw = " ".join([f"{b:02x}" for b in self.frame])
|
||||
return f"{dt} Received {size} bytes{channel}: {raw}"
|
||||
return f"{c_datetime} Received {size} bytes{channel}: {raw}"
|
||||
|
||||
class HoymilesNRF:
|
||||
"""Hoymiles NRF24 Interface"""
|
||||
|
@ -322,6 +335,7 @@ class HoymilesNRF:
|
|||
|
||||
has_payload, pipe_number = self.radio.available_pipe()
|
||||
if has_payload:
|
||||
|
||||
# Data in nRF24 buffer, read it
|
||||
self.rx_error = 0
|
||||
self.rx_channel_ack = True
|
||||
|
@ -334,9 +348,11 @@ class HoymilesNRF:
|
|||
ch_rx=self.rx_channel, ch_tx=self.tx_channel,
|
||||
time_rx=datetime.now()
|
||||
)
|
||||
yield(fragment)
|
||||
|
||||
yield fragment
|
||||
|
||||
else:
|
||||
|
||||
# No data in nRF rx buffer, search and wait
|
||||
# Channel lock in (not currently used)
|
||||
self.rx_error = self.rx_error + 1
|
||||
|
@ -399,7 +415,7 @@ def frame_payload(payload):
|
|||
|
||||
return payload
|
||||
|
||||
def compose_esb_fragment(fragment, seq=b'\80', src=99999999, dst=1, **params):
|
||||
def compose_esb_fragment(fragment, seq=b'\x80', src=99999999, dst=1, **params):
|
||||
"""
|
||||
Build standart ESB request fragment
|
||||
|
||||
|
@ -415,20 +431,19 @@ def compose_esb_fragment(fragment, seq=b'\80', src=99999999, dst=1, **params):
|
|||
:raises ValueError: if fragment size larger 16 byte
|
||||
"""
|
||||
if len(fragment) > 17:
|
||||
raise ValueError(f'ESB fragment exeeds mtu ({mtu}): Fragment size {len(fragment)} bytes')
|
||||
raise ValueError(f'ESB fragment exeeds mtu: Fragment size {len(fragment)} bytes')
|
||||
|
||||
p = b''
|
||||
p = p + b'\x15'
|
||||
p = p + ser_to_hm_addr(dst)
|
||||
p = p + ser_to_hm_addr(src)
|
||||
p = p + seq
|
||||
packet = b'\x15'
|
||||
packet = packet + ser_to_hm_addr(dst)
|
||||
packet = packet + ser_to_hm_addr(src)
|
||||
packet = packet + seq
|
||||
|
||||
p = p + fragment
|
||||
packet = packet + fragment
|
||||
|
||||
crc8 = f_crc8(p)
|
||||
p = p + struct.pack('B', crc8)
|
||||
crc8 = f_crc8(packet)
|
||||
packet = packet + struct.pack('B', crc8)
|
||||
|
||||
return p
|
||||
return packet
|
||||
|
||||
def compose_esb_packet(packet, mtu=17, **params):
|
||||
"""
|
||||
|
@ -441,7 +456,7 @@ def compose_esb_packet(packet, mtu=17, **params):
|
|||
"""
|
||||
for i in range(0, len(packet), mtu):
|
||||
fragment = compose_esb_fragment(packet[i:i+mtu], **params)
|
||||
yield(fragment)
|
||||
yield fragment
|
||||
|
||||
def compose_set_time_payload(timestamp=None):
|
||||
"""
|
||||
|
@ -472,6 +487,7 @@ class InverterTransaction:
|
|||
inverter_addr = None
|
||||
dtu_ser = None
|
||||
req_type = None
|
||||
time_rx = None
|
||||
|
||||
radio = None
|
||||
|
||||
|
@ -530,15 +546,15 @@ class InverterTransaction:
|
|||
if not self.radio:
|
||||
return False
|
||||
|
||||
if not len(self.tx_queue):
|
||||
if len(self.tx_queue) == 0:
|
||||
return False
|
||||
|
||||
packet = self.tx_queue.pop(0)
|
||||
|
||||
if HOYMILES_TRANSACTION_LOGGING:
|
||||
dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
print(f'{dt} Transmit {len(packet)} | {hexify_payload(packet)}')
|
||||
|
||||
c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
print(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}')
|
||||
|
||||
self.radio.transmit(packet)
|
||||
|
||||
wait = False
|
||||
|
@ -546,7 +562,7 @@ class InverterTransaction:
|
|||
for response in self.radio.receive():
|
||||
if HOYMILES_TRANSACTION_LOGGING:
|
||||
print(response)
|
||||
|
||||
|
||||
self.frame_append(response)
|
||||
wait = True
|
||||
except TimeoutError:
|
||||
|
@ -646,9 +662,9 @@ class InverterTransaction:
|
|||
:return: log line of payload for transmission
|
||||
:rtype: str
|
||||
"""
|
||||
dt = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
c_datetime = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
size = len(self.request)
|
||||
return f'{dt} Transmit | {hexify_payload(self.request)}'
|
||||
return f'{c_datetime} Transmit | {hexify_payload(self.request)}'
|
||||
|
||||
def hexify_payload(byte_var):
|
||||
"""
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Hoymiles micro-inverters main application
|
||||
"""
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
import hoymiles
|
||||
from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
|
||||
import paho.mqtt.client
|
||||
import yaml
|
||||
from yaml.loader import SafeLoader
|
||||
import paho.mqtt.client
|
||||
from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
|
||||
import hoymiles
|
||||
|
||||
def main_loop():
|
||||
"""Main loop"""
|
||||
|
@ -61,14 +65,14 @@ def poll_inverter(inverter, retries=4):
|
|||
try:
|
||||
response = com.get_payload()
|
||||
payload_ttl = 0
|
||||
except Exception as e:
|
||||
print(f'Error while retrieving data: {e}')
|
||||
except Exception as e_all:
|
||||
print(f'Error while retrieving data: {e_all}')
|
||||
pass
|
||||
|
||||
# Handle the response data if any
|
||||
if response:
|
||||
dt = datetime.now()
|
||||
print(f'{dt} Payload: ' + hoymiles.hexify_payload(response))
|
||||
c_datetime = datetime.now()
|
||||
print(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
|
||||
decoder = hoymiles.ResponseDecoder(response,
|
||||
request=com.request,
|
||||
inverter_ser=inverter_ser
|
||||
|
@ -77,7 +81,7 @@ def poll_inverter(inverter, retries=4):
|
|||
if isinstance(result, hoymiles.decoders.StatusResponse):
|
||||
data = result.__dict__()
|
||||
if hoymiles.HOYMILES_DEBUG_LOGGING:
|
||||
print(f'{dt} Decoded: {data["temperature"]}', end='')
|
||||
print(f'{c_datetime} Decoded: {data["temperature"]}', end='')
|
||||
phase_id = 0
|
||||
for phase in data['phases']:
|
||||
print(f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}', end='')
|
||||
|
@ -91,6 +95,8 @@ def poll_inverter(inverter, retries=4):
|
|||
if mqtt_client:
|
||||
mqtt_send_status(mqtt_client, inverter_ser, data,
|
||||
topic=inverter.get('mqtt', {}).get('topic', None))
|
||||
if influx_client:
|
||||
influx_client.store_status(result)
|
||||
|
||||
def mqtt_send_status(broker, inverter_ser, data, topic=None):
|
||||
"""
|
||||
|
@ -183,17 +189,17 @@ if __name__ == '__main__':
|
|||
|
||||
# Load ahoy.yml config file
|
||||
try:
|
||||
if isinstance(global_config.config_file, str) == True:
|
||||
with open(global_config.config_file, 'r') as yf:
|
||||
cfg = yaml.load(yf, Loader=SafeLoader)
|
||||
if isinstance(global_config.config_file, str):
|
||||
with open(global_config.config_file, 'r') as fh_yaml:
|
||||
cfg = yaml.load(fh_yaml, Loader=SafeLoader)
|
||||
else:
|
||||
with open('ahoy.yml', 'r') as yf:
|
||||
cfg = yaml.load(yf, Loader=SafeLoader)
|
||||
with open('ahoy.yml', 'r') as fh_yaml:
|
||||
cfg = yaml.load(fh_yaml, Loader=SafeLoader)
|
||||
except FileNotFoundError:
|
||||
print("Could not load config file. Try --help")
|
||||
sys.exit(2)
|
||||
except yaml.YAMLError as ye:
|
||||
print('Failed to load config frile {global_config.config_file}: {ye}')
|
||||
except yaml.YAMLError as e_yaml:
|
||||
print('Failed to load config frile {global_config.config_file}: {e_yaml}')
|
||||
sys.exit(1)
|
||||
|
||||
ahoy_config = dict(cfg.get('ahoy', {}))
|
||||
|
@ -225,21 +231,32 @@ if __name__ == '__main__':
|
|||
mqtt_client.loop_start()
|
||||
mqtt_client.on_message = mqtt_on_command
|
||||
|
||||
influx_client = None
|
||||
influx_config = ahoy_config.get('influxdb', {})
|
||||
if influx_config and not influx_config.get('disabled', False):
|
||||
from .outputs import InfluxOutputPlugin
|
||||
influx_client = InfluxOutputPlugin(
|
||||
influx_config.get('url'),
|
||||
influx_config.get('token'),
|
||||
org=influx_config.get('org', ''),
|
||||
bucket=influx_config.get('bucket', None),
|
||||
measurement=influx_config.get('measurement', 'hoymiles'))
|
||||
|
||||
if not radio.begin():
|
||||
raise RuntimeError('Can\'t open radio')
|
||||
|
||||
inverters = [inverter.get('serial') for inverter in ahoy_config.get('inverters', [])]
|
||||
for inverter in ahoy_config.get('inverters', []):
|
||||
inverter_ser = inverter.get('serial')
|
||||
command_queue[str(inverter_ser)] = []
|
||||
g_inverters = [g_inverter.get('serial') for g_inverter in ahoy_config.get('inverters', [])]
|
||||
for g_inverter in ahoy_config.get('inverters', []):
|
||||
g_inverter_ser = g_inverter.get('serial')
|
||||
command_queue[str(g_inverter_ser)] = []
|
||||
|
||||
#
|
||||
# Enables and subscribe inverter to mqtt /command-Topic
|
||||
#
|
||||
if mqtt_client and inverter.get('mqtt', {}).get('send_raw_enabled', False):
|
||||
if mqtt_client and g_inverter.get('mqtt', {}).get('send_raw_enabled', False):
|
||||
topic_item = (
|
||||
str(inverter_ser),
|
||||
inverter.get('mqtt', {}).get('topic', f'hoymiles/{inverter_ser}') + '/command'
|
||||
str(g_inverter_ser),
|
||||
g_inverter.get('mqtt', {}).get('topic', f'hoymiles/{g_inverter_ser}') + '/command'
|
||||
)
|
||||
mqtt_client.subscribe(topic_item[1])
|
||||
mqtt_command_topic_subs.append(topic_item)
|
||||
|
|
|
@ -1,14 +1,50 @@
|
|||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Hoymiles Micro-Inverters decoder library
|
||||
"""
|
||||
|
||||
import struct
|
||||
from datetime import datetime, timedelta
|
||||
import crcmod
|
||||
from datetime import timedelta
|
||||
|
||||
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||
|
||||
class StatusResponse:
|
||||
class Response:
|
||||
""" All Response Shared methods """
|
||||
inverter_ser = None
|
||||
inverter_name = None
|
||||
dtu_ser = None
|
||||
response = None
|
||||
|
||||
def __init__(self, *args, **params):
|
||||
"""
|
||||
:param bytes response: response payload bytes
|
||||
"""
|
||||
self.inverter_ser = params.get('inverter_ser', None)
|
||||
self.inverter_name = params.get('inverter_name', None)
|
||||
self.dtu_ser = params.get('dtu_ser', None)
|
||||
|
||||
self.response = args[0]
|
||||
|
||||
if isinstance(params.get('time_rx', None), datetime):
|
||||
self.time_rx = params['time_rx']
|
||||
else:
|
||||
self.time_rx = datetime.now()
|
||||
|
||||
def __dict__(self):
|
||||
""" Base values, availabe in each __dict__ call """
|
||||
return {
|
||||
'inverter_ser': self.inverter_ser,
|
||||
'inverter_name': self.inverter_name,
|
||||
'dtu_ser': self.dtu_ser}
|
||||
|
||||
class StatusResponse(Response):
|
||||
"""Inverter StatusResponse object"""
|
||||
e_keys = ['voltage','current','power','energy_total','energy_daily']
|
||||
e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor']
|
||||
temperature = None
|
||||
frequency = None
|
||||
|
||||
def unpack(self, fmt, base):
|
||||
"""
|
||||
|
@ -77,17 +113,19 @@ class StatusResponse:
|
|||
:return: dict of properties
|
||||
:rtype: dict
|
||||
"""
|
||||
data = {}
|
||||
data = super().__dict__()
|
||||
data['phases'] = self.phases
|
||||
data['strings'] = self.strings
|
||||
data['temperature'] = self.temperature
|
||||
data['frequency'] = self.frequency
|
||||
data['time'] = self.time_rx
|
||||
return data
|
||||
|
||||
class UnknownResponse:
|
||||
class UnknownResponse(Response):
|
||||
"""
|
||||
Debugging helper for unknown payload format
|
||||
"""
|
||||
|
||||
@property
|
||||
def hex_ascii(self):
|
||||
"""
|
||||
|
@ -96,7 +134,7 @@ class UnknownResponse:
|
|||
:return: hexlifierd byte string
|
||||
:rtype: str
|
||||
"""
|
||||
return ' '.join([f'{b:02x}' for b in self.response])
|
||||
return ' '.join([f'{byte:02x}' for byte in self.response])
|
||||
|
||||
@property
|
||||
def valid_crc(self):
|
||||
|
@ -113,116 +151,117 @@ class UnknownResponse:
|
|||
@property
|
||||
def dump_longs(self):
|
||||
"""Get all data, interpreted as long"""
|
||||
if len(self.response) < 5:
|
||||
if len(self.response) < 3:
|
||||
return None
|
||||
|
||||
res = self.response
|
||||
|
||||
r = len(res) % 16
|
||||
res = res[:r*-1]
|
||||
rem = len(res) % 16
|
||||
res = res[:rem*-1]
|
||||
|
||||
vals = None
|
||||
if len(res) % 16 == 0:
|
||||
n = len(res)/4
|
||||
vals = struct.unpack(f'>{int(n)}L', res)
|
||||
rlen = len(res)/4
|
||||
vals = struct.unpack(f'>{int(rlen)}L', res)
|
||||
|
||||
return vals
|
||||
|
||||
@property
|
||||
def dump_longs_pad1(self):
|
||||
"""Get all data, interpreted as long"""
|
||||
if len(self.response) < 7:
|
||||
if len(self.response) < 5:
|
||||
return None
|
||||
|
||||
res = self.response[2:]
|
||||
|
||||
r = len(res) % 16
|
||||
res = res[:r*-1]
|
||||
rem = len(res) % 16
|
||||
res = res[:rem*-1]
|
||||
|
||||
vals = None
|
||||
if len(res) % 16 == 0:
|
||||
n = len(res)/4
|
||||
vals = struct.unpack(f'>{int(n)}L', res)
|
||||
rlen = len(res)/4
|
||||
vals = struct.unpack(f'>{int(rlen)}L', res)
|
||||
|
||||
return vals
|
||||
|
||||
@property
|
||||
def dump_longs_pad2(self):
|
||||
"""Get all data, interpreted as long"""
|
||||
if len(self.response) < 9:
|
||||
if len(self.response) < 7:
|
||||
return None
|
||||
|
||||
res = self.response[4:]
|
||||
|
||||
r = len(res) % 16
|
||||
res = res[:r*-1]
|
||||
rem = len(res) % 16
|
||||
res = res[:rem*-1]
|
||||
|
||||
vals = None
|
||||
if len(res) % 16 == 0:
|
||||
n = len(res)/4
|
||||
vals = struct.unpack(f'>{int(n)}L', res)
|
||||
rlen = len(res)/4
|
||||
vals = struct.unpack(f'>{int(rlen)}L', res)
|
||||
|
||||
return vals
|
||||
|
||||
@property
|
||||
def dump_longs_pad3(self):
|
||||
"""Get all data, interpreted as long"""
|
||||
if len(self.response) < 11:
|
||||
if len(self.response) < 9:
|
||||
return None
|
||||
|
||||
res = self.response[6:]
|
||||
|
||||
r = len(res) % 16
|
||||
res = res[:r*-1]
|
||||
rem = len(res) % 16
|
||||
res = res[:rem*-1]
|
||||
|
||||
vals = None
|
||||
if len(res) % 16 == 0:
|
||||
n = len(res)/4
|
||||
vals = struct.unpack(f'>{int(n)}L', res)
|
||||
rlen = len(res)/4
|
||||
vals = struct.unpack(f'>{int(rlen)}L', res)
|
||||
|
||||
return vals
|
||||
|
||||
@property
|
||||
def dump_shorts(self):
|
||||
"""Get all data, interpreted as short"""
|
||||
if len(self.response) < 5:
|
||||
if len(self.response) < 3:
|
||||
return None
|
||||
|
||||
res = self.response
|
||||
|
||||
r = len(res) % 4
|
||||
res = res[:r*-1]
|
||||
rem = len(res) % 4
|
||||
res = res[:rem*-1]
|
||||
|
||||
vals = None
|
||||
if len(res) % 4 == 0:
|
||||
n = len(res)/2
|
||||
vals = struct.unpack(f'>{int(n)}H', res)
|
||||
rlen = len(res)/2
|
||||
vals = struct.unpack(f'>{int(rlen)}H', res)
|
||||
|
||||
return vals
|
||||
|
||||
@property
|
||||
def dump_shorts_pad1(self):
|
||||
"""Get all data, interpreted as short"""
|
||||
if len(self.response) < 6:
|
||||
if len(self.response) < 4:
|
||||
return None
|
||||
|
||||
res = self.response[1:]
|
||||
|
||||
r = len(res) % 4
|
||||
res = res[:r*-1]
|
||||
rem = len(res) % 4
|
||||
res = res[:rem*-1]
|
||||
|
||||
vals = None
|
||||
if len(res) % 4 == 0:
|
||||
n = len(res)/2
|
||||
vals = struct.unpack(f'>{int(n)}H', res)
|
||||
rlen = len(res)/2
|
||||
vals = struct.unpack(f'>{int(rlen)}H', res)
|
||||
|
||||
return vals
|
||||
|
||||
class EventsResponse(UnknownResponse):
|
||||
""" Hoymiles micro-inverter event log decode helper """
|
||||
|
||||
alarm_codes = {
|
||||
1: 'Inverter start',
|
||||
2: 'Producing power',
|
||||
2: 'DTU command failed',
|
||||
121: 'Over temperature protection',
|
||||
125: 'Grid configuration parameter error',
|
||||
126: 'Software error code 126',
|
||||
|
@ -291,21 +330,21 @@ class EventsResponse(UnknownResponse):
|
|||
9000: 'Microinverter is suspected of being stolen'
|
||||
}
|
||||
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
def __init__(self, *args, **params):
|
||||
super().__init__(*args, **params)
|
||||
|
||||
crc_valid = self.valid_crc
|
||||
if crc_valid:
|
||||
print(' payload has valid modbus crc')
|
||||
self.response = response[:-2]
|
||||
self.response = self.response[:-2]
|
||||
|
||||
status = self.response[:2]
|
||||
|
||||
chunk_size = 12
|
||||
for c in range(2, len(self.response), chunk_size):
|
||||
chunk = self.response[c:c+chunk_size]
|
||||
for i_chunk in range(2, len(self.response), chunk_size):
|
||||
chunk = self.response[i_chunk:i_chunk+chunk_size]
|
||||
|
||||
print(' '.join([f'{b:02x}' for b in chunk]) + ': ')
|
||||
print(' '.join([f'{byte:02x}' for byte in chunk]) + ': ')
|
||||
|
||||
opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6])
|
||||
a_text = self.alarm_codes.get(a_code, 'N/A')
|
||||
|
@ -316,20 +355,16 @@ class EventsResponse(UnknownResponse):
|
|||
print(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk)))
|
||||
print(end='', flush=True)
|
||||
|
||||
class DEBUG_DecodeAny(UnknownResponse):
|
||||
class DebugDecodeAny(UnknownResponse):
|
||||
"""Default decoder"""
|
||||
def __init__(self, response):
|
||||
"""
|
||||
Try interpret and print unknown response data
|
||||
|
||||
:param bytes response: response payload bytes
|
||||
"""
|
||||
self.response = response
|
||||
def __init__(self, *args, **params):
|
||||
super().__init__(*args, **params)
|
||||
|
||||
crc_valid = self.valid_crc
|
||||
if crc_valid:
|
||||
print(' payload has valid modbus crc')
|
||||
self.response = response[:-2]
|
||||
self.response = self.response[:-2]
|
||||
|
||||
l_payload = len(self.response)
|
||||
print(f' payload has {l_payload} bytes')
|
||||
|
@ -384,204 +419,247 @@ class DEBUG_DecodeAny(UnknownResponse):
|
|||
|
||||
|
||||
# 1121-Series Intervers, 1 MPPT, 1 Phase
|
||||
class HM300_Decode0B(StatusResponse):
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
class Hm300Decode0B(StatusResponse):
|
||||
""" 1121-series mirco-inverters status data """
|
||||
|
||||
@property
|
||||
def dc_voltage_0(self):
|
||||
""" String 1 VDC """
|
||||
return self.unpack('>H', 2)[0]/10
|
||||
@property
|
||||
def dc_current_0(self):
|
||||
""" String 1 ampere """
|
||||
return self.unpack('>H', 4)[0]/100
|
||||
@property
|
||||
def dc_power_0(self):
|
||||
""" String 1 watts """
|
||||
return self.unpack('>H', 6)[0]/10
|
||||
@property
|
||||
def dc_energy_total_0(self):
|
||||
""" String 1 total energy in Wh """
|
||||
return self.unpack('>L', 8)[0]
|
||||
@property
|
||||
def dc_energy_daily_0(self):
|
||||
""" String 1 daily energy in Wh """
|
||||
return self.unpack('>H', 12)[0]
|
||||
|
||||
|
||||
@property
|
||||
def ac_voltage_0(self):
|
||||
""" Phase 1 VAC """
|
||||
return self.unpack('>H', 14)[0]/10
|
||||
@property
|
||||
def ac_current_0(self):
|
||||
""" Phase 1 ampere """
|
||||
return self.unpack('>H', 22)[0]/100
|
||||
@property
|
||||
def ac_power_0(self):
|
||||
""" Phase 1 watts """
|
||||
return self.unpack('>H', 18)[0]/10
|
||||
@property
|
||||
def frequency(self):
|
||||
""" Grid frequency in Hertz """
|
||||
return self.unpack('>H', 16)[0]/100
|
||||
@property
|
||||
def temperature(self):
|
||||
""" Inverter temperature in °C """
|
||||
return self.unpack('>H', 26)[0]/10
|
||||
|
||||
class HM300_Decode11(EventsResponse):
|
||||
def __init__(self, response):
|
||||
super().__init__(response)
|
||||
|
||||
class HM300_Decode12(EventsResponse):
|
||||
def __init__(self, response):
|
||||
super().__init__(response)
|
||||
class Hm300Decode11(EventsResponse):
|
||||
""" Inverter generic events log """
|
||||
|
||||
class Hm300Decode12(EventsResponse):
|
||||
""" Inverter major events log """
|
||||
|
||||
|
||||
# 1141-Series Inverters, 2 MPPT, 1 Phase
|
||||
class HM600_Decode0B(StatusResponse):
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
class Hm600Decode0B(StatusResponse):
|
||||
""" 1141-series mirco-inverters status data """
|
||||
|
||||
@property
|
||||
def dc_voltage_0(self):
|
||||
""" String 1 VDC """
|
||||
return self.unpack('>H', 2)[0]/10
|
||||
@property
|
||||
def dc_current_0(self):
|
||||
""" String 1 ampere """
|
||||
return self.unpack('>H', 4)[0]/100
|
||||
@property
|
||||
def dc_power_0(self):
|
||||
""" String 1 watts """
|
||||
return self.unpack('>H', 6)[0]/10
|
||||
@property
|
||||
def dc_energy_total_0(self):
|
||||
""" String 1 total energy in Wh """
|
||||
return self.unpack('>L', 14)[0]
|
||||
@property
|
||||
def dc_energy_daily_0(self):
|
||||
""" String 1 daily energy in Wh """
|
||||
return self.unpack('>H', 22)[0]
|
||||
|
||||
@property
|
||||
def dc_voltage_1(self):
|
||||
""" String 2 VDC """
|
||||
return self.unpack('>H', 8)[0]/10
|
||||
@property
|
||||
def dc_current_1(self):
|
||||
""" String 2 ampere """
|
||||
return self.unpack('>H', 10)[0]/100
|
||||
@property
|
||||
def dc_power_1(self):
|
||||
""" String 2 watts """
|
||||
return self.unpack('>H', 12)[0]/10
|
||||
@property
|
||||
def dc_energy_total_1(self):
|
||||
""" String 2 total energy in Wh """
|
||||
return self.unpack('>L', 18)[0]
|
||||
@property
|
||||
def dc_energy_daily_1(self):
|
||||
""" String 2 daily energy in Wh """
|
||||
return self.unpack('>H', 24)[0]
|
||||
|
||||
@property
|
||||
def ac_voltage_0(self):
|
||||
""" Phase 1 VAC """
|
||||
return self.unpack('>H', 26)[0]/10
|
||||
@property
|
||||
def ac_current_0(self):
|
||||
""" Phase 1 ampere """
|
||||
return self.unpack('>H', 34)[0]/10
|
||||
@property
|
||||
def ac_power_0(self):
|
||||
""" Phase 1 watts """
|
||||
return self.unpack('>H', 30)[0]/10
|
||||
@property
|
||||
def frequency(self):
|
||||
""" Grid frequency in Hertz """
|
||||
return self.unpack('>H', 28)[0]/100
|
||||
@property
|
||||
def temperature(self):
|
||||
""" Inverter temperature in °C """
|
||||
return self.unpack('>H', 38)[0]/10
|
||||
@property
|
||||
def alarm_count(self):
|
||||
""" Event counter """
|
||||
return self.unpack('>H', 40)[0]
|
||||
|
||||
class HM600_Decode11(EventsResponse):
|
||||
def __init__(self, response):
|
||||
super().__init__(response)
|
||||
class Hm600Decode11(EventsResponse):
|
||||
""" Inverter generic events log """
|
||||
|
||||
class HM600_Decode12(EventsResponse):
|
||||
def __init__(self, response):
|
||||
super().__init__(response)
|
||||
class Hm600Decode12(EventsResponse):
|
||||
""" Inverter major events log """
|
||||
|
||||
|
||||
# 1161-Series Inverters, 4 MPPT, 1 Phase
|
||||
class HM1200_Decode0B(StatusResponse):
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
class Hm1200Decode0B(StatusResponse):
|
||||
""" 1161-series mirco-inverters status data """
|
||||
|
||||
@property
|
||||
def dc_voltage_0(self):
|
||||
""" String 1 VDC """
|
||||
return self.unpack('>H', 2)[0]/10
|
||||
@property
|
||||
def dc_current_0(self):
|
||||
""" String 1 ampere """
|
||||
return self.unpack('>H', 4)[0]/100
|
||||
@property
|
||||
def dc_power_0(self):
|
||||
""" String 1 watts """
|
||||
return self.unpack('>H', 8)[0]/10
|
||||
@property
|
||||
def dc_energy_total_0(self):
|
||||
""" String 1 total energy in Wh """
|
||||
return self.unpack('>L', 12)[0]
|
||||
@property
|
||||
def dc_energy_daily_0(self):
|
||||
""" String 1 daily energy in Wh """
|
||||
return self.unpack('>H', 20)[0]
|
||||
|
||||
@property
|
||||
def dc_voltage_1(self):
|
||||
""" String 2 VDC """
|
||||
return self.unpack('>H', 2)[0]/10
|
||||
@property
|
||||
def dc_current_1(self):
|
||||
""" String 2 ampere """
|
||||
return self.unpack('>H', 4)[0]/100
|
||||
@property
|
||||
def dc_power_1(self):
|
||||
""" String 2 watts """
|
||||
return self.unpack('>H', 10)[0]/10
|
||||
@property
|
||||
def dc_energy_total_1(self):
|
||||
""" String 2 total energy in Wh """
|
||||
return self.unpack('>L', 16)[0]
|
||||
@property
|
||||
def dc_energy_daily_1(self):
|
||||
""" String 2 daily energy in Wh """
|
||||
return self.unpack('>H', 22)[0]
|
||||
|
||||
@property
|
||||
def dc_voltage_2(self):
|
||||
""" String 3 VDC """
|
||||
return self.unpack('>H', 24)[0]/10
|
||||
@property
|
||||
def dc_current_2(self):
|
||||
""" String 3 ampere """
|
||||
return self.unpack('>H', 26)[0]/100
|
||||
@property
|
||||
def dc_power_2(self):
|
||||
""" String 3 watts """
|
||||
return self.unpack('>H', 30)[0]/10
|
||||
@property
|
||||
def dc_energy_total_2(self):
|
||||
""" String 3 total energy in Wh """
|
||||
return self.unpack('>L', 34)[0]
|
||||
@property
|
||||
def dc_energy_daily_2(self):
|
||||
""" String 3 daily energy in Wh """
|
||||
return self.unpack('>H', 42)[0]
|
||||
|
||||
@property
|
||||
def dc_voltage_3(self):
|
||||
""" String 4 VDC """
|
||||
return self.unpack('>H', 24)[0]/10
|
||||
@property
|
||||
def dc_current_3(self):
|
||||
""" String 4 ampere """
|
||||
return self.unpack('>H', 28)[0]/100
|
||||
@property
|
||||
def dc_power_3(self):
|
||||
""" String 4 watts """
|
||||
return self.unpack('>H', 32)[0]/10
|
||||
@property
|
||||
def dc_energy_total_3(self):
|
||||
""" String 4 total energy in Wh """
|
||||
return self.unpack('>L', 38)[0]
|
||||
@property
|
||||
def dc_energy_daily_3(self):
|
||||
""" String 4 daily energy in Wh """
|
||||
return self.unpack('>H', 44)[0]
|
||||
|
||||
@property
|
||||
def ac_voltage_0(self):
|
||||
""" Phase 1 VAC """
|
||||
return self.unpack('>H', 46)[0]/10
|
||||
@property
|
||||
def ac_current_0(self):
|
||||
""" Phase 1 ampere """
|
||||
return self.unpack('>H', 54)[0]/100
|
||||
@property
|
||||
def ac_power_0(self):
|
||||
""" Phase 1 watts """
|
||||
return self.unpack('>H', 50)[0]/10
|
||||
@property
|
||||
def frequency(self):
|
||||
""" Grid frequency in Hertz """
|
||||
return self.unpack('>H', 48)[0]/100
|
||||
@property
|
||||
def temperature(self):
|
||||
""" Inverter temperature in °C """
|
||||
return self.unpack('>H', 58)[0]/10
|
||||
|
||||
class HM1200_Decode11(EventsResponse):
|
||||
def __init__(self, response):
|
||||
super().__init__(response)
|
||||
class Hm1200Decode11(EventsResponse):
|
||||
""" Inverter generic events log """
|
||||
|
||||
class HM1200_Decode12(EventsResponse):
|
||||
def __init__(self, response):
|
||||
super().__init__(response)
|
||||
class Hm1200Decode12(EventsResponse):
|
||||
""" Inverter major events log """
|
||||
|
|
197
tools/rpi/hoymiles/outputs.py
Normal file
197
tools/rpi/hoymiles/outputs.py
Normal file
|
@ -0,0 +1,197 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Hoymiles output plugin library
|
||||
"""
|
||||
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
from hoymiles.decoders import StatusResponse
|
||||
|
||||
try:
|
||||
from influxdb_client import InfluxDBClient
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
class OutputPluginFactory:
|
||||
def __init__(self, **params):
|
||||
"""
|
||||
Initialize output plugin
|
||||
|
||||
:param inverter_ser: The inverter serial
|
||||
:type inverter_ser: str
|
||||
:param inverter_name: The configured name for the inverter
|
||||
:type inverter_name: str
|
||||
"""
|
||||
|
||||
self.inverter_ser = params.get('inverter_ser', '')
|
||||
self.inverter_name = params.get('inverter_name', None)
|
||||
|
||||
def store_status(self, response, **params):
|
||||
"""
|
||||
Default function
|
||||
|
||||
:raises NotImplementedError: when the plugin does not implement store status data
|
||||
"""
|
||||
raise NotImplementedError('The current output plugin does not implement store_status')
|
||||
|
||||
class InfluxOutputPlugin(OutputPluginFactory):
|
||||
""" Influx2 output plugin """
|
||||
api = None
|
||||
|
||||
def __init__(self, url, token, **params):
|
||||
"""
|
||||
Initialize InfluxOutputPlugin
|
||||
|
||||
The following targets must be present in your InfluxDB. This does not
|
||||
automatically create anything for You.
|
||||
|
||||
:param str url: The url to connect this client to. Like http://localhost:8086
|
||||
:param str token: Influx2 access token which is allowed to write to bucket
|
||||
:param org: Influx2 org, the token belongs to
|
||||
:type org: str
|
||||
:param bucket: Influx2 bucket to store data in (also known as retention policy)
|
||||
:type bucket: str
|
||||
:param measurement: Default measurement-prefix to use
|
||||
:type measurement: str
|
||||
"""
|
||||
super().__init__(**params)
|
||||
|
||||
self._bucket = params.get('bucket', 'hoymiles/autogen')
|
||||
self._org = params.get('org', '')
|
||||
self._measurement = params.get('measurement',
|
||||
f'inverter,host={socket.gethostname()}')
|
||||
|
||||
client = InfluxDBClient(url, token, bucket=self._bucket)
|
||||
self.api = client.write_api()
|
||||
|
||||
def store_status(self, response, **params):
|
||||
"""
|
||||
Publish StatusResponse object
|
||||
|
||||
:param hoymiles.decoders.StatusResponse response: StatusResponse object
|
||||
:type response: hoymiles.decoders.StatusResponse
|
||||
:param measurement: Custom influx measurement name
|
||||
:type measurement: str or None
|
||||
|
||||
:raises ValueError: when response is not instance of StatusResponse
|
||||
"""
|
||||
|
||||
if not isinstance(response, StatusResponse):
|
||||
raise ValueError('Data needs to be instance of StatusResponse')
|
||||
|
||||
data = response.__dict__()
|
||||
|
||||
measurement = self._measurement + f',location={data["inverter_ser"]}'
|
||||
|
||||
data_stack = []
|
||||
|
||||
time_rx = datetime.now()
|
||||
if 'time' in data and isinstance(data['time'], datetime):
|
||||
time_rx = data['time']
|
||||
|
||||
# InfluxDB uses UTC
|
||||
utctime = datetime.fromtimestamp(time_rx.timestamp(), tz=timezone.utc)
|
||||
|
||||
# InfluxDB requires nanoseconds
|
||||
ctime = int(utctime.timestamp() * 1e9)
|
||||
|
||||
# AC Data
|
||||
phase_id = 0
|
||||
for phase in data['phases']:
|
||||
data_stack.append(f'{measurement},phase={phase_id},type=power value={phase["power"]} {ctime}')
|
||||
data_stack.append(f'{measurement},phase={phase_id},type=voltage value={phase["voltage"]} {ctime}')
|
||||
data_stack.append(f'{measurement},phase={phase_id},type=current value={phase["current"]} {ctime}')
|
||||
phase_id = phase_id + 1
|
||||
|
||||
# DC Data
|
||||
string_id = 0
|
||||
for string in data['strings']:
|
||||
data_stack.append(f'{measurement},string={string_id},type=total value={string["energy_total"]/1000:.4f} {ctime}')
|
||||
data_stack.append(f'{measurement},string={string_id},type=power value={string["power"]:.2f} {ctime}')
|
||||
data_stack.append(f'{measurement},string={string_id},type=voltage value={string["voltage"]:.3f} {ctime}')
|
||||
data_stack.append(f'{measurement},string={string_id},type=current value={string["current"]:3f} {ctime}')
|
||||
string_id = string_id + 1
|
||||
# Global
|
||||
data_stack.append(f'{measurement},type=frequency value={data["frequency"]:.3f} {ctime}')
|
||||
data_stack.append(f'{measurement},type=temperature value={data["temperature"]:.2f} {ctime}')
|
||||
|
||||
self.api.write(self._bucket, self._org, data_stack)
|
||||
|
||||
try:
|
||||
import paho.mqtt.client
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
class MqttOutputPlugin(OutputPluginFactory):
|
||||
""" Mqtt output plugin """
|
||||
client = None
|
||||
|
||||
def __init__(self, *args, **params):
|
||||
"""
|
||||
Initialize MqttOutputPlugin
|
||||
|
||||
:param host: Broker ip or hostname (defaults to: 127.0.0.1)
|
||||
:type host: str
|
||||
:param port: Broker port
|
||||
:type port: int (defaults to: 1883)
|
||||
:param user: Optional username to login to the broker
|
||||
:type user: str or None
|
||||
:param password: Optional passwort to login to the broker
|
||||
:type password: str or None
|
||||
:param topic: Topic prefix to use (defaults to: hoymiles/{inverter_ser})
|
||||
:type topic: str
|
||||
|
||||
:param paho.mqtt.client.Client broker: mqtt-client instance
|
||||
:param str inverter_ser: inverter serial
|
||||
:param hoymiles.StatusResponse data: decoded inverter StatusResponse
|
||||
:param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser})
|
||||
:type topic: str
|
||||
"""
|
||||
super().__init__(*args, **params)
|
||||
|
||||
mqtt_client = paho.mqtt.client.Client()
|
||||
mqtt_client.username_pw_set(params.get('user', None), params.get('password', None))
|
||||
mqtt_client.connect(params.get('host', '127.0.0.1'), params.get('port', 1883))
|
||||
mqtt_client.loop_start()
|
||||
|
||||
self.client = mqtt_client
|
||||
|
||||
def store_status(self, response, **params):
|
||||
"""
|
||||
Publish StatusResponse object
|
||||
|
||||
:param hoymiles.decoders.StatusResponse response: StatusResponse object
|
||||
:param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser})
|
||||
:type topic: str
|
||||
|
||||
:raises ValueError: when response is not instance of StatusResponse
|
||||
"""
|
||||
|
||||
if not isinstance(response, StatusResponse):
|
||||
raise ValueError('Data needs to be instance of StatusResponse')
|
||||
|
||||
data = response.__dict__()
|
||||
|
||||
topic = params.get('topic', f'hoymiles/{data["inverter_ser"]}')
|
||||
|
||||
# AC Data
|
||||
phase_id = 0
|
||||
for phase in data['phases']:
|
||||
self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power'])
|
||||
self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'])
|
||||
self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'])
|
||||
phase_id = phase_id + 1
|
||||
|
||||
# DC Data
|
||||
string_id = 0
|
||||
for string in data['strings']:
|
||||
self.client.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000)
|
||||
self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'])
|
||||
self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'])
|
||||
self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'])
|
||||
string_id = string_id + 1
|
||||
# Global
|
||||
self.client.publish(f'{topic}/frequency', data['frequency'])
|
||||
self.client.publish(f'{topic}/temperature', data['temperature'])
|
1
tools/rpi/optional-requirements.txt
Normal file
1
tools/rpi/optional-requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
influxdb-client>=1.28.0
|
Loading…
Add table
Add a link
Reference in a new issue