diff --git a/tasmota/xnrg_12_solaxX1.ino b/tasmota/xnrg_12_solaxX1.ino index fa925e519..9be2e7194 100644 --- a/tasmota/xnrg_12_solaxX1.ino +++ b/tasmota/xnrg_12_solaxX1.ino @@ -1,7 +1,8 @@ /* xnrg_12_solaxX1.ino - Solax X1 inverter RS485 support for Tasmota - Copyright (C) 2021 Pablo Zerón + Copyright (C) 2021 by Pablo Zerón + Copyright (C) 2022 by Stefan Wershoven This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -35,11 +36,18 @@ #include -enum solaxX1_Error -{ - solaxX1_ERR_NO_ERROR, - solaxX1_ERR_CRC_ERROR -}; +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_SOLAX_MODE_5 "|" D_SOLAX_MODE_6; + +const char kSolaxError[] PROGMEM = + D_SOLAX_ERROR_0 "|" D_SOLAX_ERROR_1 "|" D_SOLAX_ERROR_2 "|" D_SOLAX_ERROR_3 "|" D_SOLAX_ERROR_4 "|" D_SOLAX_ERROR_5 "|" + D_SOLAX_ERROR_6 "|" D_SOLAX_ERROR_7 "|" D_SOLAX_ERROR_8; + +const char kSolaxSafetyType[] PROGMEM = + "VDE0126|ARN4105|AS4777_AU|G98/1|C10/11|OVE/ONORME8001|EN50438_NL|EN50438_DK|CEB|CEI021|NRS097_2_1|" + "VDE0126_Gr_Is|UTE_C15_712|IEC61727|G99/1|VDE0126_Gr_Co|France_VFR2014|C15_712_is_50|C15_712_is_60|" + "AS4777_NZ|RD1699|Chile|EN50438_Ireland|Philippines|Czech_PPDS|Czech_50438"; union { uint32_t ErrMessage; @@ -81,20 +89,9 @@ union { uint8_t OtherDeviceFault:1;//30 uint8_t ErrBit31:1;//31 }; -} ErrCode; +} solaxX1_ErrCode; -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_SOLAX_MODE_5 "|" D_SOLAX_MODE_6; - -const char kSolaxError[] PROGMEM = - D_SOLAX_ERROR_0 "|" D_SOLAX_ERROR_1 "|" D_SOLAX_ERROR_2 "|" D_SOLAX_ERROR_3 "|" D_SOLAX_ERROR_4 "|" D_SOLAX_ERROR_5 "|" - D_SOLAX_ERROR_6 "|" D_SOLAX_ERROR_7 "|" D_SOLAX_ERROR_8; - -/*********************************************************************************************/ - -TasmotaSerial *solaxX1Serial; - -struct SOLAXX1 { +struct SOLAXX1_LIVEDATA { int16_t temperature = 0; float energy_today = 0; float dc1_voltage = 0; @@ -104,295 +101,364 @@ struct SOLAXX1 { uint32_t runtime_total = 0; float dc1_power = 0; float dc2_power = 0; - int16_t runMode = 0; uint32_t errorCode = 0; + uint8_t SerialNumber[16] = {0x6e, 0x2f, 0x61}; // "n/a" } solaxX1; -union { - uint8_t status; - struct { - uint8_t freeBit7:1; // Bit7 - uint8_t freeBit6:1; // Bit6 - uint8_t freeBit5:1; // Bit5 - uint8_t queryOffline:1; // Bit4 - uint8_t queryOfflineSend:1; // Bit3 - uint8_t hasAddress:1; // Bit2 - uint8_t inverterAddressSend:1; // Bit1 - uint8_t inverterSnReceived:1; // Bit0 - }; -} protocolStatus; +struct SOLAXX1_GLOBALDATA { + bool AddressAssigned = true; + uint8_t SendRetry_count = 20; + uint8_t QueryData_count = 0; + uint8_t QueryID_count = 240; + bool Command_QueryID = false;; + bool Command_QueryConfig = false; +} solaxX1_global; -uint8_t header[2] = {0xAA, 0x55}; -uint8_t source[2] = {0x00, 0x00}; -uint8_t destination[2] = {0x00, 0x00}; -uint8_t controlCode[1] = {0x00}; -uint8_t functionCode[1] = {0x00}; -uint8_t dataLength[1] = {0x00}; -uint8_t data[16] = {0}; +struct SOLAXX1_SENDDATA { + uint8_t Header[2] = {0xAA, 0x55}; + uint8_t Source[2] = {0x00, 0x00}; + uint8_t Destination[2] = {0x00, 0x00}; + uint8_t ControlCode[1] = {0x00}; + uint8_t FunctionCode[1] = {0x00}; + uint8_t DataLength[1] = {0x00}; + uint8_t Payload[16] = {0}; +} solaxX1_SendData; -uint8_t message[30]; +TasmotaSerial *solaxX1Serial; /*********************************************************************************************/ -bool solaxX1_RS485ReceiveReady(void) +void solaxX1_RS485Send(void) { - return (solaxX1Serial->available() > 1); -} - -void solaxX1_RS485Send(uint16_t msgLen) -{ - memcpy(message, header, 2); - memcpy(message + 2, source, 2); - memcpy(message + 4, destination, 2); - memcpy(message + 6, controlCode, 1); - memcpy(message + 7, functionCode, 1); - memcpy(message + 8, dataLength, 1); - memcpy(message + 9, data, sizeof(data)); - uint16_t crc = solaxX1_calculateCRC(message, msgLen); // calculate out crc bytes - - while (solaxX1Serial->available() > 0) - { // read serial if any old data is available + uint8_t message[30]; + memcpy(message, solaxX1_SendData.Header, 2); + memcpy(message + 2, solaxX1_SendData.Source, 2); + memcpy(message + 4, solaxX1_SendData.Destination, 2); + memcpy(message + 6, solaxX1_SendData.ControlCode, 1); + 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 solaxX1Serial->read(); } - if (PinUsed(GPIO_SOLAXX1_RTS)) { digitalWrite(Pin(GPIO_SOLAXX1_RTS), HIGH); } solaxX1Serial->flush(); - solaxX1Serial->write(message, msgLen); + 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, msgLen); + AddLogBuffer(LOG_LEVEL_DEBUG_MORE, message, 9 + solaxX1_SendData.DataLength[0]); } -uint8_t solaxX1_RS485Receive(uint8_t *value) +bool solaxX1_RS485Receive(uint8_t *ReadBuffer) { uint8_t len = 0; - - while (solaxX1Serial->available() > 0) - { - value[len++] = (uint8_t)solaxX1Serial->read(); - } - - AddLogBuffer(LOG_LEVEL_DEBUG_MORE, value, len); - - uint16_t crc = solaxX1_calculateCRC(value, len - 2); // calculate out crc bytes - - if (value[len - 1] == lowByte(crc) && value[len - 2] == highByte(crc)) - { // check calc crc with received crc - return solaxX1_ERR_NO_ERROR; - } - else - { - return solaxX1_ERR_CRC_ERROR; + while (solaxX1Serial->available() > 0) { + ReadBuffer[len++] = (uint8_t)solaxX1Serial->read(); } + AddLogBuffer(LOG_LEVEL_DEBUG_MORE, ReadBuffer, len); + uint16_t crc = solaxX1_calculateCRC(ReadBuffer, len - 2); // calculate out crc bytes + return !(ReadBuffer[len - 1] == lowByte(crc) && ReadBuffer[len - 2] == highByte(crc)); } uint16_t solaxX1_calculateCRC(uint8_t *bExternTxPackage, uint8_t bLen) { uint8_t i; - uint16_t wChkSum; - wChkSum = 0; - - for (i = 0; i < bLen; i++) - { + uint16_t wChkSum = 0; + for (i = 0; i < bLen; i++) { wChkSum = wChkSum + bExternTxPackage[i]; } return wChkSum; } +void solaxX1_ExtractText(uint8_t *DataIn, uint8_t *DataOut, uint8_t Begin, uint8_t End) +{ + uint8_t i; + for (i = Begin; i <= End; i++) { + DataOut[i - Begin] = DataIn[i]; + } + DataOut[End - Begin + 1] = 0; +} + +void solaxX1_QueryOfflineInverters(void) +{ + solaxX1_SendData.Source[0] = 0x01; + solaxX1_SendData.Destination[0] = 0x00; + solaxX1_SendData.Destination[1] = 0x00; + solaxX1_SendData.ControlCode[0] = 0x10; + solaxX1_SendData.FunctionCode[0] = 0x00; + solaxX1_SendData.DataLength[0] = 0x00; + solaxX1_RS485Send(); +} + void solaxX1_SendInverterAddress(void) { - source[0] = 0x00; - destination[0] = 0x00; - destination[1] = 0x00; - controlCode[0] = 0x10; - functionCode[0] = 0x01; - dataLength[0] = 0x0F; - data[14] = INVERTER_ADDRESS; // Inverter Address, It must be unique in case of more inverters in the same rs485 net. - solaxX1_RS485Send(24); + solaxX1_SendData.Source[0] = 0x00; + solaxX1_SendData.Destination[0] = 0x00; + solaxX1_SendData.Destination[1] = 0x00; + solaxX1_SendData.ControlCode[0] = 0x10; + solaxX1_SendData.FunctionCode[0] = 0x01; + solaxX1_SendData.DataLength[0] = 0x0F; + solaxX1_SendData.Payload[14] = INVERTER_ADDRESS; // Inverter Address, It must be unique in case of more inverters in the same rs485 net. + solaxX1_RS485Send(); } void solaxX1_QueryLiveData(void) { - source[0] = 0x01; - destination[0] = 0x00; - destination[1] = INVERTER_ADDRESS; - controlCode[0] = 0x11; - functionCode[0] = 0x02; - dataLength[0] = 0x00; - solaxX1_RS485Send(9); + solaxX1_SendData.Source[0] = 0x01; + solaxX1_SendData.Destination[0] = 0x00; + solaxX1_SendData.Destination[1] = INVERTER_ADDRESS; + solaxX1_SendData.ControlCode[0] = 0x11; + solaxX1_SendData.FunctionCode[0] = 0x02; + solaxX1_SendData.DataLength[0] = 0x00; + solaxX1_RS485Send(); +} + +void solaxX1_QueryIDData(void) +{ + solaxX1_SendData.Source[0] = 0x01; + solaxX1_SendData.Destination[0] = 0x00; + solaxX1_SendData.Destination[1] = INVERTER_ADDRESS; + solaxX1_SendData.ControlCode[0] = 0x11; + solaxX1_SendData.FunctionCode[0] = 0x03; + solaxX1_SendData.DataLength[0] = 0x00; + solaxX1_RS485Send(); +} + +void solaxX1_QueryConfigData(void) +{ + solaxX1_SendData.Source[0] = 0x01; + solaxX1_SendData.Destination[0] = 0x00; + solaxX1_SendData.Destination[1] = INVERTER_ADDRESS; + solaxX1_SendData.ControlCode[0] = 0x11; + solaxX1_SendData.FunctionCode[0] = 0x04; + solaxX1_SendData.DataLength[0] = 0x00; + solaxX1_RS485Send(); } uint8_t solaxX1_ParseErrorCode(uint32_t code){ - ErrCode.ErrMessage = code; - + solaxX1_ErrCode.ErrMessage = code; if (code == 0) return 0; - if (ErrCode.MainsLostFault) return 1; - if (ErrCode.GridVoltFault) return 2; - if (ErrCode.GridFreqFault) return 3; - if (ErrCode.PvVoltFault) return 4; - if (ErrCode.IsolationFault) return 5; - if (ErrCode.TemperatureOverFault) return 6; - if (ErrCode.FanFault) return 7; - if (ErrCode.OtherDeviceFault) return 8; + if (solaxX1_ErrCode.MainsLostFault) return 1; + if (solaxX1_ErrCode.GridVoltFault) return 2; + if (solaxX1_ErrCode.GridFreqFault) return 3; + if (solaxX1_ErrCode.PvVoltFault) return 4; + if (solaxX1_ErrCode.IsolationFault) return 5; + if (solaxX1_ErrCode.TemperatureOverFault) return 6; + if (solaxX1_ErrCode.FanFault) return 7; + if (solaxX1_ErrCode.OtherDeviceFault) return 8; return 0; } /*********************************************************************************************/ -uint8_t solaxX1_send_retry = 20; -uint8_t solaxX1_queryData_count = 0; - -void solaxX1250MSecond(void) // Every 250 milliseconds +void solaxX1_250MSecond(void) // Every 250 milliseconds { - uint8_t value[61] = {0}; - bool data_ready = solaxX1_RS485ReceiveReady(); + uint8_t DataRead[80] = {0}; + uint8_t TempData[16] = {0}; + char TempDataChar[16]; + float TempFloat; - if (data_ready) - { - if (protocolStatus.hasAddress) - { - uint8_t error = solaxX1_RS485Receive(value); - if (error) - { - DEBUG_SENSOR_LOG(PSTR("SX1: Data response CRC error")); - } - else - { - solaxX1_send_retry = 20; - Energy.data_valid[0] = 0; - - solaxX1.temperature = (value[9] << 8) | value[10]; // Temperature - solaxX1.energy_today = (float)((value[11] << 8) | value[12]) * 0.1f; // Energy Today - solaxX1.dc1_voltage = (float)((value[13] << 8) | value[14]) * 0.1f; // PV1 Voltage - solaxX1.dc2_voltage = (float)((value[15] << 8) | value[16]) * 0.1f; // PV2 Voltage - solaxX1.dc1_current = (float)((value[17] << 8) | value[18]) * 0.1f; // PV1 Current - solaxX1.dc2_current = (float)((value[19] << 8) | value[20]) * 0.1f; // PV2 Current - Energy.current[0] = (float)((value[21] << 8) | value[22]) * 0.1f; // AC Current - Energy.voltage[0] = (float)((value[23] << 8) | value[24]) * 0.1f; // AC Voltage - Energy.frequency[0] = (float)((value[25] << 8) | value[26]) * 0.01f; // AC Frequency - Energy.active_power[0] = (float)((value[27] << 8) | value[28]); // AC Power - //temporal = (float)((value[29] << 8) | value[30]) * 0.1f; // Not Used - Energy.import_active[0] = (float)((value[31] << 24) | (value[32] << 16) | (value[33] << 8) | value[34]) * 0.1f; // Energy Total - solaxX1.runtime_total = ((value[35] << 24) | (value[36] << 16) | (value[37] << 8) | value[38]); // Work Time Total - solaxX1.runMode = (value[39] << 8) | value[40]; // Work mode - //temporal = (float)((value[41] << 8) | value[42]); // Grid voltage fault value 0.1V - //temporal = (float)((value[43] << 8) | value[44]); // Gird frequency fault value 0.01Hz - //temporal = (float)((value[45] << 8) | value[46]); // Dc injection fault value 1mA - //temporal = (float)((value[47] << 8) | value[48]); // Temperature fault value - //temporal = (float)((value[49] << 8) | value[50]); // Pv1 voltage fault value 0.1V - //temporal = (float)((value[51] << 8) | value[52]); // Pv2 voltage fault value 0.1V - //temporal = (float)((value[53] << 8) | value[54]); // GFC fault value - solaxX1.errorCode = (value[58] << 24) | (value[57] << 16) | (value[56] << 8) | value[55]; // Error Code - - solaxX1.dc1_power = solaxX1.dc1_voltage * solaxX1.dc1_current; - solaxX1.dc2_power = solaxX1.dc2_voltage * solaxX1.dc2_current; - - EnergyUpdateTotal(); // 484.708 kWh - } - } else { // end hasAddress - // check address confirmation from inverter - if (protocolStatus.inverterAddressSend) - { - uint8_t error = solaxX1_RS485Receive(value); - if (error) - { - DEBUG_SENSOR_LOG(PSTR("SX1: Address confirmation response CRC error")); - } - else - { - if (value[6] == 0x10 && value[7] == 0x81 && value[9] == 0x06) - { - DEBUG_SENSOR_LOG(PSTR("SX1: Set hasAddress")); - protocolStatus.status = 0b00100000; // hasAddress - } - } - } - - // Check inverter serial number and send the set address request - if (protocolStatus.queryOfflineSend) - { - uint8_t error = solaxX1_RS485Receive(value); - if (error) - { - DEBUG_SENSOR_LOG(PSTR("SX1: Query Offline response CRC error")); - } - else - { - // Serial number from query response - if (value[6] == 0x10 && value[7] == 0x80 && protocolStatus.inverterSnReceived == false) - { - for (uint8_t i = 9; i <= 22; i++) - { - data[i - 9] = value[i]; - } - solaxX1_SendInverterAddress(); - protocolStatus.status = 0b1100000; // inverterSnReceived and inverterAddressSend - DEBUG_SENSOR_LOG(PSTR("SX1: Set inverterSnReceived and inverterAddressSend")); - } - } - } + if (solaxX1Serial->available()) { + if (solaxX1_RS485Receive(DataRead)) { // CRC-error -> no further action + DEBUG_SENSOR_LOG(PSTR("SX1: Data response CRC error")); + return; } - } + + solaxX1_global.SendRetry_count = 20; // Inverter is responding - if (protocolStatus.hasAddress) { - if (solaxX1_queryData_count <= 0) { - solaxX1_queryData_count = 5; - DEBUG_SENSOR_LOG(PSTR("SX1: Send Retry count: %d"), solaxX1_send_retry); - solaxX1_QueryLiveData(); + if (DataRead[0] != 0xAA || DataRead[1] != 0x55) { // Check for header + DEBUG_SENSOR_LOG(PSTR("SX1: Check for header failed")); + return; } - solaxX1_queryData_count--; - } - // request to the inverter the serial number if offline - if (protocolStatus.queryOffline) - { - // We sent the message to query inverters in offline status - source[0] = 0x01; - destination[1] = 0x00; - controlCode[0] = 0x10; - functionCode[0] = 0x00; - dataLength[0] = 0x00; - solaxX1_RS485Send(9); - protocolStatus.status = 0b00010000; // queryOfflineSend - solaxX1_send_retry = 20; - DEBUG_SENSOR_LOG(PSTR("SX1: Query Offline Send")); - } + 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 + solaxX1.energy_today = ((DataRead[11] << 8) | DataRead[12]) * 0.1f; // Energy Today + solaxX1.dc1_voltage = ((DataRead[13] << 8) | DataRead[14]) * 0.1f; // PV1 Voltage + solaxX1.dc2_voltage = ((DataRead[15] << 8) | DataRead[16]) * 0.1f; // PV2 Voltage + solaxX1.dc1_current = ((DataRead[17] << 8) | DataRead[18]) * 0.1f; // PV1 Current + solaxX1.dc2_current = ((DataRead[19] << 8) | DataRead[20]) * 0.1f; // PV2 Current + Energy.current[0] = ((DataRead[21] << 8) | DataRead[22]) * 0.1f; // AC Current + Energy.voltage[0] = ((DataRead[23] << 8) | DataRead[24]) * 0.1f; // AC Voltage + Energy.frequency[0] = ((DataRead[25] << 8) | DataRead[26]) * 0.01f; // AC Frequency + Energy.active_power[0] = ((DataRead[27] << 8) | DataRead[28]); // AC Power + //temporal = (float)((DataRead[29] << 8) | DataRead[30]) * 0.1f; // Not Used + Energy.import_active[0] = ((DataRead[31] << 24) | (DataRead[32] << 16) | (DataRead[33] << 8) | DataRead[34]) * 0.1f; // Energy Total + solaxX1.runtime_total = (DataRead[35] << 24) | (DataRead[36] << 16) | (DataRead[37] << 8) | DataRead[38]; // Work Time Total + solaxX1.runMode = (DataRead[39] << 8) | DataRead[40]; // Work mode + //temporal = (float)((DataRead[41] << 8) | DataRead[42]); // Grid voltage fault value 0.1V + //temporal = (float)((DataRead[43] << 8) | DataRead[44]); // Gird frequency fault value 0.01Hz + //temporal = (float)((DataRead[45] << 8) | DataRead[46]); // Dc injection fault value 1mA + //temporal = (float)((DataRead[47] << 8) | DataRead[48]); // Temperature fault value + //temporal = (float)((DataRead[49] << 8) | DataRead[50]); // Pv1 voltage fault value 0.1V + //temporal = (float)((DataRead[51] << 8) | DataRead[52]); // Pv2 voltage fault value 0.1V + //temporal = (float)((DataRead[53] << 8) | DataRead[54]); // GFC fault value + solaxX1.errorCode = (DataRead[58] << 24) | (DataRead[57] << 16) | (DataRead[56] << 8) | DataRead[55]; // Error Code + solaxX1.dc1_power = solaxX1.dc1_voltage * solaxX1.dc1_current; + solaxX1.dc2_power = solaxX1.dc2_voltage * solaxX1.dc2_current; + EnergyUpdateTotal(); // 484.708 kWh + DEBUG_SENSOR_LOG(PSTR("SX1: received live data")); + return; + } // end received "Response for query (live data)" - if (solaxX1_send_retry == 0) { - if (protocolStatus.hasAddress) { - protocolStatus.status = 0b00001000; // queryOffline + if (DataRead[6] == 0x11 && DataRead[7] == 0x83) { // received "Response for query (ID data)" + solaxX1_ExtractText(DataRead, solaxX1.SerialNumber, 49, 62); // extract "real" serial number + if (solaxX1_global.Command_QueryID) { + AddLog(LOG_LEVEL_INFO, PSTR("SX1: Inverter phases: %d"),DataRead[9]); // number of phases + solaxX1_ExtractText(DataRead, TempData, 10, 15); // extract rated bus power (my be empty) + AddLog(LOG_LEVEL_INFO, PSTR("SX1: Inverter rated bus power: %s"),(char*)TempData); + solaxX1_ExtractText(DataRead, TempData, 16, 20); // extract firmware version + AddLog(LOG_LEVEL_INFO, PSTR("SX1: Inverter firmware version: %s"),(char*)TempData); + solaxX1_ExtractText(DataRead, TempData, 21, 34); // extract module name (my be empty) + AddLog(LOG_LEVEL_INFO, PSTR("SX1: Inverter module name: %s"),(char*)TempData); + solaxX1_ExtractText(DataRead, TempData, 35, 48); // extract factory name + AddLog(LOG_LEVEL_INFO, PSTR("SX1: Inverter factory name: %s"),(char*)TempData); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: Inverter serial number: %s"),(char*)solaxX1.SerialNumber); + solaxX1_ExtractText(DataRead, TempData, 63, 66); // extract rated bus voltage + AddLog(LOG_LEVEL_INFO, PSTR("SX1: Inverter rated bus voltage: %s"),(char*)TempData); + solaxX1_global.Command_QueryID = false; + } else { + AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: Inverter serial number: %s"),(char*)solaxX1.SerialNumber); + } + DEBUG_SENSOR_LOG(PSTR("SX1: received ID data")); + return; + } // end received "Response for query (ID data)" + + if (DataRead[6] == 0x11 && DataRead[7] == 0x84) { // received "Response for query (config data)" + if (solaxX1_global.Command_QueryConfig) { + // This values are displayed as they were received from the inverter. They are not interpreted in any way. + TempFloat = ((DataRead[9] << 8) | DataRead[10]) * 0.1f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: new wVpvStart: %1_f V (Inverter launch voltage threshold)"), &TempFloat); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wTimeStart: %d sec (launch wait time)"), (DataRead[11] << 8) | DataRead[12]); + TempFloat = ((DataRead[13] << 8) | DataRead[14]) * 0.1f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wVacMinProtect: %1_f V (allowed minimum grid voltage)"), &TempFloat); + TempFloat = ((DataRead[15] << 8) | DataRead[16]) * 0.1f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wVacMaxProtect: %1_f V (allowed maximum grid voltage)"), &TempFloat); + TempFloat = ((DataRead[17] << 8) | DataRead[18]) * 0.01f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wFacMinProtect: %2_f Hz (allowed minimum grid frequency)"), &TempFloat); + TempFloat = ((DataRead[19] << 8) | DataRead[20]) * 0.01f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wFacMaxProtect: %2_f Hz (allowed maximum grid frequency)"), &TempFloat); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wDciLimits: %d mA (DC component limits)"), (DataRead[21] << 8) | DataRead[22]); + TempFloat = ((DataRead[23] << 8) | DataRead[24]) * 0.1f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wGrid10MinAvgProtect: %1_f V (10 minutes over voltage protect)"), &TempFloat); + TempFloat = ((DataRead[25] << 8) | DataRead[26]) * 0.1f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wVacMinSlowProtect: %1_f V (grid undervoltage protect value)"), &TempFloat); + TempFloat = ((DataRead[27] << 8) | DataRead[28]) * 0.1f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wVacMaxSlowProtect: %1_f V (grid overvoltage protect value)"), &TempFloat); + TempFloat = ((DataRead[29] << 8) | DataRead[30]) * 0.01f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wFacMinSlowProtect: %2_f Hz (grid underfrequency protect value)"), &TempFloat); + TempFloat = ((DataRead[31] << 8) | DataRead[32]) * 0.01f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wFacMaxSlowProtect: %2_f Hz (grid overfrequency protect value)"), &TempFloat); + GetTextIndexed(TempDataChar, sizeof(TempDataChar), (DataRead[33] << 8) | DataRead[34], kSolaxSafetyType); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wSafety: %d ≙ %s"), (DataRead[33] << 8) | DataRead[34], TempDataChar); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wPowerfactor_mode: %d"), DataRead[35]); + TempFloat = DataRead[36] * 0.01f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wPowerfactor_data: %2_f"), &TempFloat); + TempFloat = DataRead[37] * 0.01f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wUpperLimit: %2_f (overexcite limits)"), &TempFloat); + TempFloat = DataRead[38] * 0.01f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wLowerLimit: %2_f (underexcite limits)"), &TempFloat); + TempFloat = DataRead[39] * 0.01f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wPowerLow: %2_f (power ratio change upper limits)"), &TempFloat); + TempFloat = DataRead[40] * 0.01f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wPowerUp: %2_f (power ratio change lower limits)"), &TempFloat); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: Qpower_set: %d"), (DataRead[41] << 8) | DataRead[42]); + TempFloat = ((DataRead[43] << 8) | DataRead[44]) * 0.01f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: WFreqSetPoint: %2_f Hz (Over Frequency drop output setpoint)"), &TempFloat); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: WFreqDroopRate: %d %% (drop output slope)"), (DataRead[45] << 8) | DataRead[46]); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: QuVupRate: %d %% (Q(U) curve up set point)"), (DataRead[47] << 8) | DataRead[48]); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: QuVlowRate: %d %% (Q(U) curve low set point)"), (DataRead[49] << 8) | DataRead[50]); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: WPowerLimitsPercent: %d %%"), (DataRead[51] << 8) | DataRead[52]); + TempFloat = ((DataRead[53] << 8) | DataRead[54]) * 0.01f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: WWgra: %2_f %%"), &TempFloat); + TempFloat = ((DataRead[55] << 8) | DataRead[56]) * 0.1f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wWv2: %1_f V"), &TempFloat); + TempFloat = ((DataRead[57] << 8) | DataRead[58]) * 0.1f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wWv3: %1_f V"), &TempFloat); + TempFloat = ((DataRead[59] << 8) | DataRead[60]) * 0.1f; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wWv4: %1_f V"), &TempFloat); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wQurangeV1: %d %%"), (DataRead[61] << 8) | DataRead[62]); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: wQurangeV4: %d %%"), (DataRead[63] << 8) | DataRead[64]); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: BVoltPowerLimit: %d"), (DataRead[65] << 8) | DataRead[66]); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: WPowerManagerEnable: %d"), (DataRead[67] << 8) | DataRead[68]); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: WGlobalSeachMPPTStrartFlg: %d"), (DataRead[69] << 8) | DataRead[70]); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: WFrqProtectRestrictive: %d"), (DataRead[71] << 8) | DataRead[72]); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: WQuDelayTimer: %d sec"), (DataRead[73] << 8) | DataRead[74]); + AddLog(LOG_LEVEL_INFO, PSTR("SX1: WFreqActivePowerDelayTimer: %d ms"), (DataRead[75] << 8) | DataRead[76]); + + solaxX1_global.Command_QueryConfig = false; + } + DEBUG_SENSOR_LOG(PSTR("SX1: received config data")); + return; + } // end received "Response for query (config data)" + + if (DataRead[6] == 0x10 && DataRead[7] == 0x80) { // received "register request" + solaxX1_global.QueryData_count = 5; // give time for next query + solaxX1_ExtractText(DataRead, solaxX1_SendData.Payload, 9, 22); // store serial number for register + DEBUG_SENSOR_LOG(PSTR("SX1: received register request and send register address")); + solaxX1_SendInverterAddress(); // "send register address" + return; + } + + if (DataRead[6] == 0x10 && DataRead[7] == 0x81 && DataRead[9] == 0x06) { // received "address confirm (ACK)" + solaxX1_global.QueryData_count = 5; // give time for next query + solaxX1_global.AddressAssigned = true; + DEBUG_SENSOR_LOG(PSTR("SX1: received \"address confirm (ACK)\"")); + return; + } + + } // end solaxX1Serial->available() + +// 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 + solaxX1_global.QueryData_count = 5; + if (!solaxX1_global.QueryID_count || solaxX1_global.Command_QueryID) { // ID query + DEBUG_SENSOR_LOG(PSTR("SX1: Send ID query")); + solaxX1_QueryIDData(); + } else if (solaxX1_global.Command_QueryConfig) { // Config query + DEBUG_SENSOR_LOG(PSTR("SX1: Send config query")); + solaxX1_QueryConfigData(); + } else { // live query + DEBUG_SENSOR_LOG(PSTR("SX1: Send live query")); + solaxX1_QueryLiveData(); + } + solaxX1_global.QueryID_count++; // query ID every 256th time + } // end normal periodically query + solaxX1_global.QueryData_count--; + if (!solaxX1_global.SendRetry_count) { // Inverter went "off" + 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.runMode = -1; // off(line) - } else { - if (protocolStatus.queryOfflineSend) { - protocolStatus.status = 0b00001000; // queryOffline - DEBUG_SENSOR_LOG(PSTR("SX1: Set Query Offline")); - } - solaxX1_send_retry = 20; + solaxX1_global.AddressAssigned = false; + } // end Inverter went "off" + } else { // sent query for inverters in offline status + if (!solaxX1_global.SendRetry_count) { + solaxX1_global.SendRetry_count = 20; + DEBUG_SENSOR_LOG(PSTR("SX1: Sent query for inverters in offline state")); + solaxX1_QueryOfflineInverters(); } } + solaxX1_global.SendRetry_count--; - if (!data_ready && solaxX1_send_retry > 0) { solaxX1_send_retry--; } -} +return; +} // end solaxX1_250MSecond -void solaxX1SnsInit(void) +void solaxX1_SnsInit(void) { - AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: Solax X1 Inverter Init")); - AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: RX-pin: %d, TX-pin: %d, RTS-pin: %d"), Pin(GPIO_SOLAXX1_RX), Pin(GPIO_SOLAXX1_TX), Pin(GPIO_SOLAXX1_RTS)); -// DEBUG_SENSOR_LOG(PSTR("SX1: RX pin: %d, TX pin: %d"), Pin(GPIO_SOLAXX1_RX), Pin(GPIO_SOLAXX1_TX)); - protocolStatus.status = 0b00100000; // hasAddress - + 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)); solaxX1Serial = new TasmotaSerial(Pin(GPIO_SOLAXX1_RX), Pin(GPIO_SOLAXX1_TX), 1); if (solaxX1Serial->begin(SOLAXX1_SPEED)) { if (solaxX1Serial->hardwareSerial()) { ClaimSerial(); } @@ -404,13 +470,33 @@ void solaxX1SnsInit(void) } } -void solaxX1DrvInit(void) +void solaxX1_DrvInit(void) { if (PinUsed(GPIO_SOLAXX1_RX) && PinUsed(GPIO_SOLAXX1_TX)) { TasmotaGlobal.energy_driver = XNRG_12; } } +bool SolaxX1_cmd(void) +{ + if (!solaxX1_global.AddressAssigned) { + AddLog(LOG_LEVEL_INFO, PSTR("SX1: No inverter registered")); + return false; + } + + 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")) { + solaxX1_global.Command_QueryConfig = true; + AddLog(LOG_LEVEL_INFO, PSTR("SX1: ReadConfig sent...")); + return true; + } + AddLog(LOG_LEVEL_INFO, PSTR("SX1: Unknown command: \"%s\""),XdrvMailbox.data); + return false; +} + #ifdef USE_WEBSERVER const char HTTP_SNS_solaxX1_DATA1[] PROGMEM = "{s}" D_SOLAX_X1 " " D_SOLAR_POWER "{m}%s " D_UNIT_WATT "{e}" @@ -426,10 +512,11 @@ const char HTTP_SNS_solaxX1_DATA2[] PROGMEM = const char HTTP_SNS_solaxX1_DATA3[] PROGMEM = "{s}" D_SOLAX_X1 " " D_UPTIME "{m}%s " D_UNIT_HOUR "{e}" "{s}" D_SOLAX_X1 " " D_STATUS "{m}%s" - "{s}" D_SOLAX_X1 " " D_ERROR "{m}%s"; + "{s}" D_SOLAX_X1 " " D_ERROR "{m}%s" + "{s}" D_SOLAX_X1 " Inverter SN{m}%s"; #endif // USE_WEBSERVER -void solaxX1Show(bool json) +void solaxX1_Show(bool json) { char solar_power[33]; dtostrfd(solaxX1.dc1_power + solaxX1.dc2_power, Settings->flag2.wattage_resolution, solar_power); @@ -452,8 +539,7 @@ void solaxX1Show(bool json) char status[33]; GetTextIndexed(status, sizeof(status), solaxX1.runMode + 1, kSolaxMode); - if (json) - { + if (json) { ResponseAppend_P(PSTR(",\"" D_JSON_SOLAR_POWER "\":%s,\"" D_JSON_PV1_VOLTAGE "\":%s,\"" D_JSON_PV1_CURRENT "\":%s,\"" D_JSON_PV1_POWER "\":%s"), solar_power, pv1_voltage, pv1_current, pv1_power); #ifdef SOLAXX1_PV2 @@ -477,7 +563,8 @@ void solaxX1Show(bool json) WSContentSend_Temp(D_SOLAX_X1, solaxX1.temperature); char errorCodeString[33]; WSContentSend_PD(HTTP_SNS_solaxX1_DATA3, runtime, status, - GetTextIndexed(errorCodeString, sizeof(errorCodeString), solaxX1_ParseErrorCode(solaxX1.errorCode), kSolaxError)); + GetTextIndexed(errorCodeString, sizeof(errorCodeString), solaxX1_ParseErrorCode(solaxX1.errorCode), kSolaxError), + solaxX1.SerialNumber); #endif // USE_WEBSERVER } } @@ -492,21 +579,24 @@ bool Xnrg12(uint8_t function) switch (function) { case FUNC_EVERY_250_MSECOND: - solaxX1250MSecond(); + solaxX1_250MSecond(); break; case FUNC_JSON_APPEND: - solaxX1Show(1); + solaxX1_Show(1); break; #ifdef USE_WEBSERVER case FUNC_WEB_SENSOR: - solaxX1Show(0); + solaxX1_Show(0); break; #endif // USE_WEBSERVER case FUNC_INIT: - solaxX1SnsInit(); + solaxX1_SnsInit(); break; case FUNC_PRE_INIT: - solaxX1DrvInit(); + solaxX1_DrvInit(); + break; + case FUNC_COMMAND: + result = SolaxX1_cmd(); break; } return result;