/* xnrg_10_sdm630.ino - Eastron SDM630-Modbus energy meter support for Tasmota Copyright (C) 2021 Gennaro Tortone and Theo Arends 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 the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_ENERGY_SENSOR #ifdef USE_SDM630 /*********************************************************************************************\ * Eastron SDM630-Modbus energy meter * * Based on: https://github.com/reaper7/SDM_Energy_Meter \*********************************************************************************************/ #define XNRG_10 10 // can be user defined in my_user_config.h #ifndef SDM630_SPEED #define SDM630_SPEED 9600 // default SDM630 Modbus address #endif // can be user defined in my_user_config.h #ifndef SDM630_ADDR #define SDM630_ADDR 1 // default SDM630 Modbus address #endif #include TasmotaModbus *Sdm630Modbus; #ifdef SDM630_HIGH_UPDATE_RATE struct sSdm630RequestConfig{ uint16_t startAddress; uint8_t registerToRead; // according to spec: max 80 register can be read a once }; const struct sSdm630RequestConfig sdm630ReqConf[] { {0x0000, 18*2}, // 0x0000 - 0x0025 {0x0046, 1*2}, // 0x0046 {0x0156, 8*2} // 0x0156 - 0x0165 }; struct SDM630 { uint8_t read_state = 0; uint8_t send_retry = 0; } Sdm630; /* convert data buffer to float value according to IEEE754 */ float convBufToFloat(uint8_t *buffer) { float value; ((uint8_t*)&value)[3] = buffer[0]; // Get float values ((uint8_t*)&value)[2] = buffer[1]; ((uint8_t*)&value)[1] = buffer[2]; ((uint8_t*)&value)[0] = buffer[3]; return value; } #else const uint16_t sdm630_start_addresses[] { // 3P4 3P3 1P2 Unit Description 0x0000, // + - + V Phase 1 line to neutral volts 0x0002, // + - - V Phase 2 line to neutral volts 0x0004, // + - - V Phase 3 line to neutral volts 0x0006, // + + + A Phase 1 current 0x0008, // + + - A Phase 2 current 0x000A, // + + - A Phase 3 current 0x000C, // + - + W Phase 1 power 0x000E, // + - + W Phase 2 power 0x0010, // + - - W Phase 3 power 0x0018, // + - + var Phase 1 volt amps reactive 0x001A, // + - - var Phase 2 volt amps reactive 0x001C, // + - - var Phase 3 volt amps reactive 0x001E, // + - + Phase 1 power factor 0x0020, // + - - Phase 2 power factor 0x0022, // + - - Phase 3 power factor 0x0046, // + + + Hz Frequency of supply voltages 0x0160, // + + + kWh Phase 1 export active energy 0x0162, // + + + kWh Phase 2 export active energy 0x0164, // + + + kWh Phase 3 export active energy //#ifdef SDM630_IMPORT 0x015A, // + + + kWh Phase 1 import active energy 0x015C, // + + + kWh Phase 2 import active energy 0x015E, // + + + kWh Phase 3 import active energy //#endif // SDM630_IMPORT 0x0156 // + + + kWh Total active energy }; struct SDM630 { uint8_t read_state = 0; uint8_t send_retry = 0; } Sdm630; #endif /*********************************************************************************************/ #ifdef SDM630_HIGH_UPDATE_RATE uint8_t sdm630ReadBuffer[128]; // at least 5 + (2*max_RegisterToRead) #endif void SDM630Every250ms(void) { bool data_ready = Sdm630Modbus->ReceiveReady(); if (data_ready) { #ifdef SDM630_HIGH_UPDATE_RATE uint8_t* buffer = &sdm630ReadBuffer[0]; uint32_t error = Sdm630Modbus->ReceiveBuffer(buffer, sdm630ReqConf[Sdm630.read_state].registerToRead); #else uint8_t buffer[14]; // At least 5 + (2 * 2) = 9 uint32_t error = Sdm630Modbus->ReceiveBuffer(buffer, 2); #endif AddLogBuffer(LOG_LEVEL_DEBUG_MORE, buffer, Sdm630Modbus->ReceiveCount()); if (error) { AddLog(LOG_LEVEL_DEBUG, PSTR("SDM: SDM630 error %d"), error); } else { Energy->data_valid[0] = 0; Energy->data_valid[1] = 0; Energy->data_valid[2] = 0; #ifndef SDM630_HIGH_UPDATE_RATE // 0 1 2 3 4 5 6 7 8 // SA FC BC Fh Fl Sh Sl Cl Ch // 01 04 04 43 66 33 34 1B 38 = 230.2 Volt float value; ((uint8_t*)&value)[3] = buffer[3]; // Get float values ((uint8_t*)&value)[2] = buffer[4]; ((uint8_t*)&value)[1] = buffer[5]; ((uint8_t*)&value)[0] = buffer[6]; #endif switch(Sdm630.read_state) { #ifdef SDM630_HIGH_UPDATE_RATE case 0: // start address 0x0000 // 3P4 3P3 1P2 Unit Description Energy->voltage[0] = convBufToFloat(&buffer[3]); // + - + V Phase 1 line to neutral volts Energy->voltage[1] = convBufToFloat(&buffer[7]); // + - - V Phase 2 line to neutral volts Energy->voltage[2] = convBufToFloat(&buffer[11]); // + - - V Phase 3 line to neutral volts //0x0006 Energy->current[0] = convBufToFloat(&buffer[15]); // + + + A Phase 1 current Energy->current[1] = convBufToFloat(&buffer[19]); // + + - A Phase 2 current Energy->current[2] = convBufToFloat(&buffer[23]); // + + - A Phase 3 current //0x000C Energy->active_power[0] = convBufToFloat(&buffer[27]); // + - + W Phase 1 power Energy->active_power[1] = convBufToFloat(&buffer[31]); // + - - W Phase 2 power Energy->active_power[2] = convBufToFloat(&buffer[35]); // + - - W Phase 3 power //0x0012 Energy->apparent_power[0] = convBufToFloat(&buffer[39]); // + - + VA Phase 1 volt amps Energy->apparent_power[1] = convBufToFloat(&buffer[43]); // + - - VA Phase 2 volt amps Energy->apparent_power[2] = convBufToFloat(&buffer[47]); // + - - VA Phase 3 volt amps //0x0018 Energy->reactive_power[0] = convBufToFloat(&buffer[51]); // + - + var Phase 1 volt amps reactive Energy->reactive_power[1] = convBufToFloat(&buffer[55]); // + - - var Phase 2 volt amps reactive Energy->reactive_power[2] = convBufToFloat(&buffer[59]); // + - - var Phase 3 volt amps reactive //0x001E Energy->power_factor[0] = convBufToFloat(&buffer[63]); // + - + Phase 1 power factor Energy->power_factor[1] = convBufToFloat(&buffer[67]); // + - - Phase 2 power factor Energy->power_factor[2] = convBufToFloat(&buffer[71]); // + - - Phase 3 power factor break; case 1: // start address 0x0046 Energy->frequency[0] = convBufToFloat(&buffer[3]); // + + + Hz Frequency of supply voltages break; case 2: // start address 0x0156 // 0x0156 // + + + kWh Total active energy // 0x0158 // + + + kvarh Total reactive energy #ifdef SDM630_IMPORT //0x015A Energy->import_active[0] = convBufToFloat(&buffer[11]); // + + + kWh Phase 1 import active energy Energy->import_active[1] = convBufToFloat(&buffer[15]); // + + + kWh Phase 2 import active energy Energy->import_active[2] = convBufToFloat(&buffer[19]); // + + + kWh Phase 3 import active energy #endif //0x0160 Energy->export_active[0] = convBufToFloat(&buffer[23]); // + + + kWh Phase 1 export active energy Energy->export_active[1] = convBufToFloat(&buffer[27]); // + + + kWh Phase 2 export active energy Energy->export_active[2] = convBufToFloat(&buffer[31]); // + + + kWh Phase 3 export active energy EnergyUpdateTotal(); break; #else //old sdm630 implementation case 0: Energy->voltage[0] = value; break; case 1: Energy->voltage[1] = value; break; case 2: Energy->voltage[2] = value; break; case 3: Energy->current[0] = value; break; case 4: Energy->current[1] = value; break; case 5: Energy->current[2] = value; break; case 6: Energy->active_power[0] = value; break; case 7: Energy->active_power[1] = value; break; case 8: Energy->active_power[2] = value; break; case 9: Energy->reactive_power[0] = value; break; case 10: Energy->reactive_power[1] = value; break; case 11: Energy->reactive_power[2] = value; break; case 12: Energy->power_factor[0] = value; break; case 13: Energy->power_factor[1] = value; break; case 14: Energy->power_factor[2] = value; break; case 15: Energy->frequency[0] = value; break; case 16: Energy->export_active[0] = value; break; case 17: Energy->export_active[1] = value; break; case 18: Energy->export_active[2] = value; break; case 19: Energy->import_active[0] = value; break; case 20: Energy->import_active[1] = value; break; case 21: Energy->import_active[2] = value; break; case 22: // Energy->import_active[0] = value; EnergyUpdateTotal(); break; #endif } Sdm630.read_state++; #ifdef SDM630_HIGH_UPDATE_RATE if ( nitems(sdm630ReqConf) == Sdm630.read_state) { #else if (sizeof(sdm630_start_addresses)/2 == Sdm630.read_state) { #endif Sdm630.read_state = 0; } } } // end data ready if (0 == Sdm630.send_retry || data_ready) { Sdm630.send_retry = 5; #ifdef SDM630_HIGH_UPDATE_RATE Sdm630Modbus->Send(SDM630_ADDR, 0x04, sdm630ReqConf[Sdm630.read_state].startAddress, sdm630ReqConf[Sdm630.read_state].registerToRead); #else Sdm630Modbus->Send(SDM630_ADDR, 0x04, sdm630_start_addresses[Sdm630.read_state], 2); #endif } else { Sdm630.send_retry--; } } void Sdm630SnsInit(void) { Sdm630Modbus = new TasmotaModbus(Pin(GPIO_SDM630_RX), Pin(GPIO_SDM630_TX), Pin(GPIO_NRG_MBS_TX_ENA)); uint8_t result = Sdm630Modbus->Begin(SDM630_SPEED); if (result) { if (2 == result) { ClaimSerial(); } Energy->phase_count = 3; Energy->frequency_common = true; // Use common frequency } else { TasmotaGlobal.energy_driver = ENERGY_NONE; } } void Sdm630DrvInit(void) { if (PinUsed(GPIO_SDM630_RX) && PinUsed(GPIO_SDM630_TX)) { TasmotaGlobal.energy_driver = XNRG_10; } } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xnrg10(uint32_t function) { bool result = false; switch (function) { case FUNC_EVERY_250_MSECOND: SDM630Every250ms(); break; case FUNC_INIT: Sdm630SnsInit(); break; case FUNC_PRE_INIT: Sdm630DrvInit(); break; } return result; } #endif // USE_SDM630 #endif // USE_ENERGY_SENSOR