[SolaxX1] Add meter mode (#22330)

This commit is contained in:
SteWers 2024-10-24 10:08:00 +02:00 committed by GitHub
parent e4f431dc7b
commit bbba5b9196
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 206 additions and 38 deletions

View File

@ -1514,9 +1514,10 @@ bool Xdrv03(uint32_t function)
case FUNC_SLEEP_LOOP:
XnrgCall(FUNC_LOOP);
break;
case FUNC_EVERY_100_MSECOND:
case FUNC_EVERY_250_MSECOND:
if (TasmotaGlobal.uptime > 4) {
XnrgCall(FUNC_EVERY_250_MSECOND);
XnrgCall(function);
}
break;
case FUNC_EVERY_SECOND:

View File

@ -1916,9 +1916,10 @@ bool Xdrv03(uint32_t function)
case FUNC_SLEEP_LOOP:
XnrgCall(FUNC_LOOP);
break;
case FUNC_EVERY_100_MSECOND:
case FUNC_EVERY_250_MSECOND:
if (TasmotaGlobal.uptime > 4) {
XnrgCall(FUNC_EVERY_250_MSECOND);
XnrgCall(function);
}
break;
case FUNC_EVERY_SECOND:

View File

@ -37,9 +37,10 @@
#define D_SOLAX_X1 "SolaxX1"
#include <TasmotaSerial.h>
TasmotaSerial *solaxX1Serial;
const char kSolaxMode[] PROGMEM =
D_OFF "|" D_SOLAX_MODE_0 "|" D_SOLAX_MODE_1 "|" D_SOLAX_MODE_2 "|" D_SOLAX_MODE_3 "|" D_SOLAX_MODE_4 "|"
D_GATEWAY "|" D_OFF "|" D_SOLAX_MODE_0 "|" D_SOLAX_MODE_1 "|" D_SOLAX_MODE_2 "|" D_SOLAX_MODE_3 "|" D_SOLAX_MODE_4 "|"
D_SOLAX_MODE_5 "|" D_SOLAX_MODE_6;
const char kSolaxError[] PROGMEM =
@ -118,6 +119,10 @@ struct SOLAXX1_GLOBALDATA {
uint8_t QueryID_count = 240;
bool Command_QueryID = false;;
bool Command_QueryConfig = false;
bool MeterMode = false;
float MeterPower = 5000;
float MeterImport;
float MeterExport;
} solaxX1_global;
struct SOLAXX1_SENDDATA {
@ -130,8 +135,6 @@ struct SOLAXX1_SENDDATA {
uint8_t Payload[16] = {0x00};
} solaxX1_SendData;
TasmotaSerial *solaxX1Serial;
/*********************************************************************************************/
void solaxX1_RS485Send(void) {
@ -143,27 +146,84 @@ void solaxX1_RS485Send(void) {
memcpy(message + 7, solaxX1_SendData.FunctionCode, 1);
memcpy(message + 8, solaxX1_SendData.DataLength, 1);
memcpy(message + 9, solaxX1_SendData.Payload, sizeof(solaxX1_SendData.Payload));
uint16_t crc = solaxX1_calculateCRC(message, 9 + solaxX1_SendData.DataLength[0]); // calculate out crc bytes
while (solaxX1Serial->available() > 0) { // read serial if any old data is available
solaxX1_RS485SendRaw(message, 9 + solaxX1_SendData.DataLength[0], 0);
}
void solaxX1_RS485SendMeterFloat(float Value) {
uint8_t MeterResponse[7] = {0x01, 0x04, 0x04, 0x00};
for (uint8_t i = 0; i <= 3; i++) { // Store bytes in reverse order
MeterResponse[i + 3] = *((char*)(&Value) + 3 - i);
}
solaxX1_RS485SendRaw(MeterResponse, 7, 1);
}
void solaxX1_RS485SendMeterInt16(int16_t Value) {
uint8_t MeterResponse[5] = {0x01, 0x03, 0x02, 0x00};
MeterResponse[3] = highByte(Value);
MeterResponse[4] = lowByte(Value);
solaxX1_RS485SendRaw(MeterResponse, 5, 1);
}
void solaxX1_RS485SendMeterTotalInt(uint32_t Export, uint32_t Import) {
uint8_t MeterResponse[11] = {0x01, 0x03, 0x08, 0x00};
for (uint8_t i = 0; i <= 3; i++) { // Store bytes in reverse order
MeterResponse[i + 3] = *((char*)(&Export) + 3 - i);
}
for (uint8_t i = 0; i <= 3; i++) { // Store bytes in reverse order
MeterResponse[i + 7] = *((char*)(&Import) + 3 - i);
}
solaxX1_RS485SendRaw(MeterResponse, 11, 1);
}
void solaxX1_RS485SendRaw(uint8_t *SendBuffer, uint8_t DataLen, uint8_t CRCflag) {
uint16_t crc;
while (solaxX1Serial->available()) { // read serial if any old data is available
solaxX1Serial->read();
}
if (PinUsed(GPIO_SOLAXX1_RTS)) {
digitalWrite(Pin(GPIO_SOLAXX1_RTS), HIGH);
if (PinUsed(GPIO_SOLAXX1_RTS)) digitalWrite(Pin(GPIO_SOLAXX1_RTS), HIGH);
solaxX1Serial->flush();
solaxX1Serial->write(SendBuffer, DataLen);
if (CRCflag) {
crc = solaxX1_calculateCRC_MBUS(SendBuffer, DataLen); // Use CRC MBUS algorithm
solaxX1Serial->write(lowByte(crc));
solaxX1Serial->write(highByte(crc));
} else {
crc = solaxX1_calculateCRC(SendBuffer, DataLen); // Use CRC Solax algorithm
solaxX1Serial->write(highByte(crc));
solaxX1Serial->write(lowByte(crc));
}
solaxX1Serial->flush();
solaxX1Serial->write(message, 9 + solaxX1_SendData.DataLength[0]);
solaxX1Serial->write(highByte(crc));
solaxX1Serial->write(lowByte(crc));
solaxX1Serial->flush();
if (PinUsed(GPIO_SOLAXX1_RTS)) {
digitalWrite(Pin(GPIO_SOLAXX1_RTS), LOW);
}
AddLogBuffer(LOG_LEVEL_DEBUG_MORE, message, 9 + solaxX1_SendData.DataLength[0]);
if (PinUsed(GPIO_SOLAXX1_RTS)) digitalWrite(Pin(GPIO_SOLAXX1_RTS), LOW);
AddLogBuffer(LOG_LEVEL_DEBUG_MORE, SendBuffer, DataLen);
}
bool solaxX1_RS485Receive(uint8_t *ReadBuffer) {
uint32_t SerWatchdogTime;
// Read header
uint8_t len = 0;
while (solaxX1Serial->available() > 0) {
SerWatchdogTime = millis();
while (len < 2) { // read exact length because of unaccurate timing of the inverter
if (solaxX1Serial->available()) ReadBuffer[len++] = (uint8_t)solaxX1Serial->read();
if (millis() > (SerWatchdogTime + 1000)) return true; // No data received -> bail out
}
// Check and set meter mode
solaxX1_SwitchMeterMode((ReadBuffer[0] == 0x01 || ReadBuffer[0] == 0x02) && (ReadBuffer[1] == 0x03 || ReadBuffer[1] == 0x04));
// Read data in meter mode
if (solaxX1_global.MeterMode) { // Metermode
SerWatchdogTime = millis();
while (len < 8) { // read exact length because of unaccurate timing of the inverter
if (solaxX1Serial->available()) ReadBuffer[len++] = (uint8_t)solaxX1Serial->read();
if (millis() > (SerWatchdogTime + 1000)) return true; // No data received -> bail out
}
AddLogBuffer(LOG_LEVEL_DEBUG_MORE, ReadBuffer, len);
return false; // Ignore checksum
} // end Metermode
// Process normal receive
while (solaxX1Serial->available()) {
ReadBuffer[len++] = (uint8_t)solaxX1Serial->read();
}
AddLogBuffer(LOG_LEVEL_DEBUG_MORE, ReadBuffer, len);
@ -175,11 +235,27 @@ uint16_t solaxX1_calculateCRC(uint8_t *bExternTxPackage, uint8_t bLen) {
uint8_t i;
uint16_t wChkSum = 0;
for (i = 0; i < bLen; i++) {
wChkSum = wChkSum + bExternTxPackage[i];
wChkSum += bExternTxPackage[i];
}
return wChkSum;
}
uint16_t solaxX1_calculateCRC_MBUS(uint8_t *frame, uint8_t Len) {
uint16_t crc = 0xFFFF;
for (uint32_t i = 0; i < Len; i++) {
crc ^= frame[i];
for (uint32_t j = 8; j; j--) {
if ((crc & 0x0001) != 0) { // If the LSB is set
crc >>= 1; // Shift right and XOR 0xA001
crc ^= 0xA001;
} else { // Else LSB is not set
crc >>= 1; // Just shift right
}
}
}
return crc;
}
void solaxX1_ExtractText(uint8_t *DataIn, uint8_t *DataOut, uint8_t Begin, uint8_t End) {
uint8_t i;
for (i = Begin; i <= End; i++) {
@ -253,27 +329,78 @@ uint8_t solaxX1_ParseErrorCode(uint32_t code) {
return 0;
}
void solaxX1_SwitchMeterMode(bool MeterMode) {
if (solaxX1_global.MeterMode == MeterMode) return;
solaxX1_global.MeterMode = MeterMode;
if (MeterMode) {
Energy->data_valid[0] = ENERGY_WATCHDOG;
solaxX1.runMode = -2;
solaxX1.temperature = solaxX1.dc1_voltage = solaxX1.dc1_current = solaxX1.dc1_power = solaxX1.dc2_voltage = solaxX1.dc2_current = solaxX1.dc2_power = 0;
} else {
solaxX1.runMode = -1;
}
}
/*********************************************************************************************/
void solaxX1_250MSecond(void) { // Every 250 milliseconds
void solaxX1_CyclicTask(void) { // Every 100/250 milliseconds
uint8_t DataRead[80] = {0};
uint8_t TempData[16] = {0};
char TempDataChar[32];
float TempFloat;
static uint32_t LastMeterTime;
static uint16_t MtrReg, MtrPwr32, MtrPwr16, MtrImp32, MtrExp32, MrtTot64, MtrRest;
if (solaxX1Serial->available()) {
if (solaxX1_RS485Receive(DataRead)) { // CRC-error -> no further action
DEBUG_SENSOR_LOG(PSTR("SX1: Data response CRC error"));
if (solaxX1_RS485Receive(DataRead)) { // CRC or other error -> no further action
AddLog(LOG_LEVEL_ERROR, PSTR("SX1: (CRC) error in received data"));
return;
}
solaxX1_global.SendRetry_count = 20; // Inverter is responding
if (DataRead[0] != 0xAA || DataRead[1] != 0x55) { // Check for header
DEBUG_SENSOR_LOG(PSTR("SX1: Check for header failed"));
if (solaxX1_global.MeterMode) { // Metermode
AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: Metermode %02X %02X xx %02X"), DataRead[0], DataRead[1], DataRead[3]);
LastMeterTime = TasmotaGlobal.uptime;
if (DataRead[0] != 0x01) return; // Respond only to requests for meter #1
switch (DataRead[3]) {
case 0x0B: // received "Register meter request"
//solaxX1_RS485SendMeterInt16(0); // Tell inverter to request int16 values
solaxX1_RS485SendMeterInt16(0xa8); // Tell inverter to request float32 values
MtrReg++;
break;
case 0x0C: // received "Power request (32 bit float)"
solaxX1_RS485SendMeterFloat(solaxX1_global.MeterPower);
MtrPwr32++;
break;
case 0x0E: // received "Power request (16 bit int)"
solaxX1_RS485SendMeterInt16((int16_t)solaxX1_global.MeterPower);
MtrPwr16++;
break;
case 0x48: // received "Import request (32 bit float)"
solaxX1_RS485SendMeterFloat(solaxX1_global.MeterImport);
MtrImp32++;
break;
case 0x4A: // received "Export request (32 bit float)"
solaxX1_RS485SendMeterFloat(solaxX1_global.MeterExport);
MtrExp32++;
break;
case 0x08: // received "Energy total request (2*32 bit uint)"
solaxX1_RS485SendMeterTotalInt((uint32_t)(solaxX1_global.MeterExport * 100.0), (uint32_t)(solaxX1_global.MeterImport * 100.0));
MrtTot64++;
break;
default:
MtrRest++;
}
AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: MtrReg %d, MtrPwr32 %d, MtrPwr16 %d, MtrImp32 %d, MtrExp32 %d, MrtTot64 %d, MtrRest %d"), MtrReg, MtrPwr32, MtrPwr16, MtrImp32, MtrExp32, MrtTot64, MtrRest);
return;
}
if (DataRead[0] != 0xAA || DataRead[1] != 0x55) { // Check for header
AddLog(LOG_LEVEL_ERROR, PSTR("SX1: Header check failed: %02X %02X"), DataRead[0], DataRead[1]);
return;
}
solaxX1_global.SendRetry_count = 20; // Inverter is responding
if (DataRead[6] == 0x11 && DataRead[7] == 0x82) { // received "Response for query (live data)"
Energy->data_valid[0] = 0;
solaxX1.temperature = (DataRead[9] << 8) | DataRead[10]; // Temperature
@ -416,6 +543,11 @@ void solaxX1_250MSecond(void) { // Every 250 milliseconds
} // end solaxX1Serial->available()
if(solaxX1_global.MeterMode) {
if (TasmotaGlobal.uptime > LastMeterTime + 20) solaxX1_SwitchMeterMode(false); // Switch back to normal mode, when no Meter request is received for 20 sec.
return;
}
// DEBUG_SENSOR_LOG(PSTR("SX1: solaxX1_global.AddressAssigned: %d, solaxX1_global.QueryData_count: %d, solaxX1_global.SendRetry_count: %d, solaxX1_global.QueryID_count: %d"), solaxX1_global.AddressAssigned, solaxX1_global.QueryData_count, solaxX1_global.SendRetry_count, solaxX1_global.QueryID_count);
if (solaxX1_global.AddressAssigned) {
if (!solaxX1_global.QueryData_count) { // normal periodically query
@ -437,13 +569,13 @@ void solaxX1_250MSecond(void) { // Every 250 milliseconds
solaxX1_global.SendRetry_count = 20;
DEBUG_SENSOR_LOG(PSTR("SX1: Inverter went \"off\""));
Energy->data_valid[0] = ENERGY_WATCHDOG;
solaxX1.temperature = solaxX1.dc1_voltage = solaxX1.dc2_voltage = solaxX1.dc1_current = solaxX1.dc2_current = solaxX1.dc1_power = 0;
solaxX1.dc2_power = Energy->current[0] = Energy->voltage[0] = Energy->frequency[0] = Energy->active_power[0] = 0;
solaxX1.temperature = solaxX1.dc1_voltage = solaxX1.dc1_current = solaxX1.dc1_power = solaxX1.dc2_voltage = solaxX1.dc2_current = solaxX1.dc2_power = 0;
solaxX1.runMode = -1; // off(line)
solaxX1_global.AddressAssigned = false;
} // end Inverter went "off"
} else { // sent query for inverters in offline status
if (!solaxX1_global.SendRetry_count) {
solaxX1_global.Command_QueryConfig = solaxX1_global.Command_QueryID = false; // Clear commands to be sure
solaxX1_global.SendRetry_count = 20;
DEBUG_SENSOR_LOG(PSTR("SX1: Sent query for inverters in offline state"));
solaxX1_QueryOfflineInverters();
@ -451,7 +583,7 @@ void solaxX1_250MSecond(void) { // Every 250 milliseconds
}
solaxX1_global.SendRetry_count--;
return;
} // end solaxX1_250MSecond
} // end solaxX1_CyclicTask
void solaxX1_SnsInit(void) {
AddLog(LOG_LEVEL_INFO, PSTR("SX1: Init - RX-pin: %d, TX-pin: %d, RTS-pin: %d"), Pin(GPIO_SOLAXX1_RX), Pin(GPIO_SOLAXX1_TX), Pin(GPIO_SOLAXX1_RTS));
@ -461,22 +593,40 @@ void solaxX1_SnsInit(void) {
#ifdef ESP32
AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: Serial UART%d"), solaxX1Serial->getUart());
#endif
if (PinUsed(GPIO_SOLAXX1_RTS)) pinMode(Pin(GPIO_SOLAXX1_RTS), OUTPUT);
} else {
TasmotaGlobal.energy_driver = ENERGY_NONE;
}
if (PinUsed(GPIO_SOLAXX1_RTS)) {
pinMode(Pin(GPIO_SOLAXX1_RTS), OUTPUT);
}
}
void solaxX1_DrvInit(void) {
if (PinUsed(GPIO_SOLAXX1_RX) && PinUsed(GPIO_SOLAXX1_TX)) {
TasmotaGlobal.energy_driver = XNRG_12;
Energy->type_dc = true; // Handle like DC, because U*I from inverter is not valid for apparent power; U*I could be lower than active power
Energy->frequency[0] = 0; // Set value, to make frequency present in output
}
}
bool SolaxX1_cmd(void) {
if (Energy->command_code != CMND_ENERGYCONFIG) return false; // Process unchanged data
if (!strncasecmp(XdrvMailbox.data, "MeterPower", 10)) {
solaxX1_global.MeterPower = CharToFloat(&XdrvMailbox.data[11]);
ResponseCmndFloat(solaxX1_global.MeterPower, 1);
AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: MeterPower: %3_f"), &solaxX1_global.MeterPower);
return false;
} else if (!strncasecmp(XdrvMailbox.data, "MeterImport", 11)) {
solaxX1_global.MeterImport = CharToFloat(&XdrvMailbox.data[12]);
ResponseCmndFloat(solaxX1_global.MeterImport, 8);
AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: MeterImport: %3_f"), &solaxX1_global.MeterImport);
return false;
} else if (!strncasecmp(XdrvMailbox.data, "MeterExport", 11)) {
solaxX1_global.MeterExport = CharToFloat(&XdrvMailbox.data[12]);
ResponseCmndFloat(solaxX1_global.MeterExport, 8);
AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: MeterExport: %3_f"), &solaxX1_global.MeterExport);
return false;
}
if (!solaxX1_global.AddressAssigned) {
AddLog(LOG_LEVEL_INFO, PSTR("SX1: No inverter registered"));
return false;
@ -484,12 +634,10 @@ bool SolaxX1_cmd(void) {
if (!strcasecmp(XdrvMailbox.data, "ReadIDinfo")) {
solaxX1_global.Command_QueryID = true;
AddLog(LOG_LEVEL_INFO, PSTR("SX1: ReadIDinfo sent..."));
return true;
} else if (!strcasecmp(XdrvMailbox.data, "ReadConfig")) {
#ifdef SOLAXX1_READCONFIG
solaxX1_global.Command_QueryConfig = true;
AddLog(LOG_LEVEL_INFO, PSTR("SX1: ReadConfig sent..."));
return true;
#else
AddLog(LOG_LEVEL_INFO, PSTR("SX1: Command not available. Please set compiler directive '#define SOLAXX1_READCONFIG'."));
@ -501,8 +649,9 @@ bool SolaxX1_cmd(void) {
}
#ifdef USE_WEBSERVER
const char HTTP_SNS_solaxX1_Num[] PROGMEM = "{s}" D_SOLAX_X1 " %s{m}</td><td style='text-align:%s'>%s{m}{m} %s{e}";
const char HTTP_SNS_solaxX1_Str[] PROGMEM = "{s}" D_SOLAX_X1 " %s{m}%s{e}";
const char HTTP_SNS_solaxX1_Num[] PROGMEM = "{s}" D_SOLAX_X1 " %s{m}</td><td style='text-align:%s'>%s{m}{m}%s{e}";
const char HTTP_SNS_solaxX1_Str[] PROGMEM = "{s}" D_SOLAX_X1 " %s</td><td style='text-align:right'>%s{e}";
const char HTTP_SNS_solaxX1_Mtr[] PROGMEM = "{s}" D_GATEWAY " %s{m}</td><td style='text-align:%s'>%s{m}{m}%s{e}";
#endif // USE_WEBSERVER
void solaxX1_Show(uint32_t function) {
@ -523,7 +672,7 @@ void solaxX1_Show(uint32_t function) {
dtostrfd(solaxX1.dc2_power, Settings->flag2.wattage_resolution, pv2_power);
#endif
char status[33];
GetTextIndexed(status, sizeof(status), solaxX1.runMode + 1, kSolaxMode);
GetTextIndexed(status, sizeof(status), solaxX1.runMode + 2, kSolaxMode);
switch (function) {
case FUNC_JSON_APPEND:
@ -544,10 +693,23 @@ void solaxX1_Show(uint32_t function) {
#ifdef USE_WEBSERVER
case FUNC_WEB_COL_SENSOR: {
String table_align = Settings->flag5.gui_table_align?"right":"left";
if (solaxX1_global.MeterMode) {
char TempDataChar[33];
WSContentSend_P(PSTR("<tr><td colspan=5 style='font-size:2px'><hr size=1/>{e}"));
dtostrfd(solaxX1_global.MeterPower, Settings->flag2.wattage_resolution, TempDataChar);
WSContentSend_PD(HTTP_SNS_solaxX1_Mtr, D_POWERUSAGE, table_align.c_str(), TempDataChar, D_UNIT_WATT);
dtostrfd(solaxX1_global.MeterImport, Settings->flag2.energy_resolution, TempDataChar);
WSContentSend_PD(HTTP_SNS_solaxX1_Mtr, "Import", table_align.c_str(), TempDataChar, D_UNIT_KILOWATTHOUR);
dtostrfd(solaxX1_global.MeterExport, Settings->flag2.energy_resolution, TempDataChar);
WSContentSend_PD(HTTP_SNS_solaxX1_Mtr, "Export", table_align.c_str(), TempDataChar, D_UNIT_KILOWATTHOUR);
return;
}
static uint32_t LastOnlineTime;
if (solaxX1.runMode != -1) LastOnlineTime = TasmotaGlobal.uptime;
if (TasmotaGlobal.uptime < LastOnlineTime + 300) { // Hide numeric live data, when inverter is offline for more than 5 min
#ifdef SOLAXX1_PV2
WSContentSend_PD(HTTP_SNS_solaxX1_Num, D_SOLAR_POWER, table_align.c_str(), solar_power, D_UNIT_WATT);
#endif
WSContentSend_PD(HTTP_SNS_solaxX1_Num, D_PV1_VOLTAGE, table_align.c_str(), pv1_voltage, D_UNIT_VOLT);
WSContentSend_PD(HTTP_SNS_solaxX1_Num, D_PV1_CURRENT, table_align.c_str(), pv1_current, D_UNIT_AMPERE);
WSContentSend_PD(HTTP_SNS_solaxX1_Num, D_PV1_POWER, table_align.c_str(), pv1_power, D_UNIT_WATT);
@ -563,6 +725,7 @@ void solaxX1_Show(uint32_t function) {
WSContentSend_P(HTTP_SNS_solaxX1_Num, D_UPTIME, table_align.c_str(), String(solaxX1.runtime_total).c_str(), D_UNIT_HOUR);
break; }
case FUNC_WEB_SENSOR:
if (solaxX1_global.MeterMode) return;
char errorCodeString[33];
WSContentSend_P(HTTP_SNS_solaxX1_Str, D_STATUS, status);
WSContentSend_P(HTTP_SNS_solaxX1_Str, D_ERROR, GetTextIndexed(errorCodeString, sizeof(errorCodeString), solaxX1_ParseErrorCode(solaxX1.errorCode), kSolaxError));
@ -580,8 +743,11 @@ bool Xnrg12(uint32_t function) {
bool result = false;
switch (function) {
case FUNC_EVERY_100_MSECOND:
if (solaxX1_global.MeterMode) solaxX1_CyclicTask();
break;
case FUNC_EVERY_250_MSECOND:
solaxX1_250MSecond();
if (!solaxX1_global.MeterMode) solaxX1_CyclicTask();
break;
#ifdef USE_WEBSERVER
case FUNC_WEB_COL_SENSOR: