/* xnrg_24_bl0906.ino - BL0906 energy sensor support for Tasmota SPDX-FileCopyrightText: 2024 Theo Arends SPDX-License-Identifier: GPL-3.0-only */ #ifdef ESP32 #ifdef USE_ENERGY_SENSOR #ifdef USE_BL0906 /*********************************************************************************************\ * Support the following Shangai Belling energy sensors: * * BL0906 - Energy (as in Athom 6CH Energy Meter EM6) * Based on athom-tech https://github.com/athom-tech/esp32-configs/tree/main/components/bl0906 * See https://github.com/arendst/Tasmota/discussions/22167 * * {"NAME":"Athom EM2","GPIO":[0,0,0,0,0,0,0,3200,11329,1,544,0,0,0,0,0,0,0,1,0,0,0],"FLAG":0,"BASE":1} * {"NAME":"Athom EM6","GPIO":[0,0,0,0,0,0,0,3200,11333,1,544,0,0,0,0,0,0,0,1,0,0,0],"FLAG":0,"BASE":1} * * Optional commands: * EnergyCols ` - Change default 4 column GUI display to columns * VoltRes 1 - Change none to 1 decimal display * FreqRes 1 - Change none to 1 decimal display * WattRes 2 - Change none to 2 decimals display * SetOption21 1 - Display Voltage * SetOption129 1 - Display energy for each phase instead of single sum * SetOption150 1 - Display no common voltage/frequency * EnergyExportActive 1 - Enable display of Export Active energy based on negative Active Power \*********************************************************************************************/ #define XNRG_24 24 #ifndef BL0906_UPDATE #define BL0906_UPDATE 2 // Update every 2 seconds (Must be lower than ENERGY_WATCHDOG) #endif //#define DEBUG_BL0906 // Total power conversion static const float BL0906_WATT = 16 * 1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / (40.41259 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000); // Total Energy conversion static const float BL0906_CF = 16 * 4194304 * 0.032768 * 16 / (3600000 * 16 * (40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 / (1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000)))); // Frequency conversion static const float BL0906_FREF = 10000000; // Temperature conversion static const float BL0906_TREF = 12.5 / 59 - 40; // Celsius // Current conversion static const float BL0906_IREF = 1.097 / (12875 * 1 * (5.1 + 5.1) * 1000 / 2000); // Voltage conversion static const float BL0906_UREF = 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / (13162 * 1 * 100 * 1000); // Power conversion static const float BL0906_PREF = 1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / (40.41259 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000); // Energy conversion static const float BL0906_EREF = 4194304 * 0.032768 * 16 / (3600000 * 16 * (40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 / (1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000)))); // Current coefficient static const float BL0906_KI = 12875 * 1 * (5.1 + 5.1) * 1000 / 2000 / 1.097; // Power coefficient static const float BL0906_KP = 40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 / 1.097 / 1.097 / (20000 + 20000 + 20000 + 20000 + 20000); // Register address // Voltage static const uint8_t BL0906_V_RMS = 0x16; // Total power static const uint8_t BL0906_WATT_SUM = 0X2C; // Current1~6 static const uint8_t BL0906_I_1_RMS = 0x0D; // current_1 static const uint8_t BL0906_I_2_RMS = 0x0E; static const uint8_t BL0906_I_3_RMS = 0x0F; static const uint8_t BL0906_I_4_RMS = 0x10; static const uint8_t BL0906_I_5_RMS = 0x13; static const uint8_t BL0906_I_6_RMS = 0x14; // current_6 // Power1~6 static const uint8_t BL0906_WATT_1 = 0X23; // power_1 static const uint8_t BL0906_WATT_2 = 0X24; static const uint8_t BL0906_WATT_3 = 0X25; static const uint8_t BL0906_WATT_4 = 0X26; static const uint8_t BL0906_WATT_5 = 0X29; static const uint8_t BL0906_WATT_6 = 0X2A; // power_6 // Active pulse count, unsigned static const uint8_t BL0906_CF_1_CNT = 0X30; // Channel_1 static const uint8_t BL0906_CF_2_CNT = 0X31; static const uint8_t BL0906_CF_3_CNT = 0X32; static const uint8_t BL0906_CF_4_CNT = 0X33; static const uint8_t BL0906_CF_5_CNT = 0X36; static const uint8_t BL0906_CF_6_CNT = 0X37; // Channel_6 // Total active pulse count, unsigned static const uint8_t BL0906_CF_SUM_CNT = 0X39; // Voltage frequency cycle static const uint8_t BL0906_FREQUENCY = 0X4E; // Internal temperature static const uint8_t BL0906_TEMPERATURE = 0X5E; // Calibration register // RMS gain adjustment register static const uint8_t BL0906_RMSGN_1 = 0x6D; // Channel_1 static const uint8_t BL0906_RMSGN_2 = 0x6E; static const uint8_t BL0906_RMSGN_3 = 0x6F; static const uint8_t BL0906_RMSGN_4 = 0x70; static const uint8_t BL0906_RMSGN_5 = 0x73; static const uint8_t BL0906_RMSGN_6 = 0x74; // Channel_6 // RMS offset correction register static const uint8_t BL0906_RMSOS_1 = 0x78; // Channel_1 static const uint8_t BL0906_RMSOS_2 = 0x79; static const uint8_t BL0906_RMSOS_3 = 0x7A; static const uint8_t BL0906_RMSOS_4 = 0x7B; static const uint8_t BL0906_RMSOS_5 = 0x7E; static const uint8_t BL0906_RMSOS_6 = 0x7F; // Channel_6 // Active power gain adjustment register static const uint8_t BL0906_WATTGN_1 = 0xB7; // Channel_1 static const uint8_t BL0906_WATTGN_2 = 0xB8; static const uint8_t BL0906_WATTGN_3 = 0xB9; static const uint8_t BL0906_WATTGN_4 = 0xBA; static const uint8_t BL0906_WATTGN_5 = 0xBD; static const uint8_t BL0906_WATTGN_6 = 0xBE; // Channel_6 // Commands static const uint8_t BL0906_READ_COMMAND = 0x35; static const uint8_t BL0906_WRITE_COMMAND = 0xCA; // User write protection setting register, // You must first write 0x5555 to the write protection setting register before writing to other registers. static const uint8_t BL0906_USR_WRPROT = 0x9E; // Enable User Operation Write static const uint8_t BL0906_WRPROT_WRITABLE[6] = {BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}; // Disable User Operation Write static const uint8_t BL0906_WRPROT_ONLYREAD[6] = {BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x00, 0x00, 0x00, 0x61}; // Reset Register static const uint8_t BL0906_SOFT_RESET = 0x9F; // Reset to default static const uint8_t BL0906_INIT[6] = {BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52}; typedef struct Bl0906DataPacket { uint8_t l{0}; uint8_t m{0}; uint8_t h{0}; uint8_t checksum; uint8_t address; } Bl0906DataPacket; typedef struct Bl0906ube24_t { uint8_t l{0}; uint8_t m{0}; uint8_t h{0}; } Bl0906ube24_t; typedef struct Bl0906sbe24_t { uint8_t l{0}; uint8_t m{0}; int8_t h{0}; } Bl0906sbe24_t; #include TasmotaSerial *Bl0906Serial = nullptr; struct BL0906 { float temperature; uint16_t baudrate; uint8_t current_channel = 0; uint8_t model = 0; uint8_t rx_pin; } Bl0906; /********************************************************************************************/ bool Bl0906_check_read_timeout(size_t len) { if (Bl0906Serial->available() >= int(len)) { return true; } uint32_t start_time = millis(); while (Bl0906Serial->available() < int(len)) { if (millis() - start_time > 100) { #ifdef DEBUG_BL0906 AddLog(LOG_LEVEL_DEBUG, PSTR("BL6: Timeout at %u/%u"), Bl0906Serial->available(), len); #endif // DEBUG_BL0906 return false; } yield(); } return true; } bool Bl0906ReadArray(uint8_t *data, size_t len) { if (!Bl0906_check_read_timeout(len)) { return false; } Bl0906Serial->read(data, len); return true; } /********************************************************************************************/ uint32_t Bl0906_to_uint32_t(Bl0906ube24_t input); uint32_t Bl0906_to_uint32_t(Bl0906ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } int32_t Bl0906_to_int32_t(Bl0906sbe24_t input); int32_t Bl0906_to_int32_t(Bl0906sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } /********************************************************************************************/ // The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated; uint8_t Bl0906Checksum(const uint8_t address, const Bl0906DataPacket *data); // Pre-declare to fix compile error on Bl0906DataPacket uint8_t Bl0906Checksum(const uint8_t address, const Bl0906DataPacket *data) { return (address + data->l + data->m + data->h) ^ 0xFF; } // RMS offset correction void Bl0906BiasCorrection(uint8_t address, float measurements, float correction) { Bl0906DataPacket data; float ki = 12875 * 1 * (5.1 + 5.1) * 1000 / 2000 / 1.097; // Current coefficient float i_rms0 = measurements * ki; float i_rms = correction * ki; int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256; data.l = value << 24 >> 24; data.m = value << 16 >> 24; if (value < 0) { data.h = (value << 8 >> 24) | 0b10000000; } data.address = Bl0906Checksum(address, &data); #ifdef DEBUG_BL0906 AddLog(LOG_LEVEL_DEBUG, PSTR("BL6: BiasCorrection %02X %02X %02X %02X %02X %02X"), BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address); #endif // DEBUG_BL0906 Bl0906Serial->write(BL0906_WRITE_COMMAND); Bl0906Serial->write(address); Bl0906Serial->write(data.l); Bl0906Serial->write(data.m); Bl0906Serial->write(data.h); Bl0906Serial->write(data.address); } /* // Gain adjustment void Bl0906GainCorrection(const uint8_t address, const float measurements, const float Correction, const float coefficient) { Bl0906DataPacket data; float I_RMS0 = measurements * coefficient; float I_RMS = Correction * coefficient; float rms_gn = int((I_RMS / I_RMS0 - 1) * 65536); int16_t value; if (rms_gn <= -32767) { value = -32767; } else { value = int(rms_gn); } data.h = 0xFF; data.m = value >> 8; data.l = value << 8 >> 8; data.address = Bl0906Checksum(address, &data); #ifdef DEBUG_BL0906 AddLog(LOG_LEVEL_DEBUG, PSTR("BL6: GainCorrection %02X %02X %02X %02X %02X %02X"), BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address); #endif // DEBUG_BL0906 Bl0906Serial->write(BL0906_WRITE_COMMAND); Bl0906Serial->write(address); Bl0906Serial->write(data.l); Bl0906Serial->write(data.m); Bl0906Serial->write(data.h); Bl0906Serial->write(data.address); } */ void Bl0906Setup(void) { while (Bl0906Serial->available()) { Bl0906Serial->flush(); } Bl0906Serial->write(BL0906_WRPROT_WRITABLE, sizeof(BL0906_WRPROT_WRITABLE)); // Calibration (1: register address; 2: value before calibration; 3: value after calibration) Bl0906BiasCorrection(BL0906_RMSOS_1, 0.01600, 0); // Calibration current_1 Bl0906BiasCorrection(BL0906_RMSOS_2, 0.01500, 0); Bl0906BiasCorrection(BL0906_RMSOS_3, 0.01400, 0); Bl0906BiasCorrection(BL0906_RMSOS_4, 0.01300, 0); Bl0906BiasCorrection(BL0906_RMSOS_5, 0.01200, 0); Bl0906BiasCorrection(BL0906_RMSOS_6, 0.01200, 0); // Calibration current_6 /* Bl0906GainCorrection(BL0906_RMSGN_1, 2.15000, 2.148, BL0906_KI); //RMS gain adjustment current_1 Bl0906GainCorrection(BL0906_RMSGN_2, 2.15100, 2.148, BL0906_KI); Bl0906GainCorrection(BL0906_RMSGN_3, 2.15200, 2.148, BL0906_KI); Bl0906GainCorrection(BL0906_RMSGN_4, 2.14500, 2.148, BL0906_KI); Bl0906GainCorrection(BL0906_RMSGN_5, 2.14600, 2.148, BL0906_KI); Bl0906GainCorrection(BL0906_RMSGN_6, 2.14600, 2.148, BL0906_KI); //RMS gain adjustment current_6 Bl0906GainCorrection(BL0906_WATTGN_1, 15.13427, 14.5, BL0906_KP); //Active power gain adjustment power_1 Bl0906GainCorrection(BL0906_WATTGN_2, 15.23937, 14.5, BL0906_KP); Bl0906GainCorrection(BL0906_WATTGN_3, 15.44956, 14.5, BL0906_KP); Bl0906GainCorrection(BL0906_WATTGN_4, 16.57646, 14.5, BL0906_KP); Bl0906GainCorrection(BL0906_WATTGN_5, 15.27440, 14.5, BL0906_KP); Bl0906GainCorrection(BL0906_WATTGN_6, 31.75744, 14.5, BL0906_KP); //Active power gain adjustment power_6 */ Bl0906Serial->write(BL0906_WRPROT_ONLYREAD, sizeof(BL0906_WRPROT_ONLYREAD)); } // Reset energy void Bl0906ResetEnergy(void) { Bl0906Serial->write(BL0906_INIT, sizeof(BL0906_INIT)); delay(1); Bl0906Serial->flush(); // Flush send buffer } // Read data void Bl0906ReadData(const uint8_t address, const float reference, float *sensor) { if (sensor == nullptr) { return; } Bl0906Serial->write(BL0906_READ_COMMAND); Bl0906Serial->write(address); Bl0906DataPacket buffer; if (Bl0906ReadArray((uint8_t *) &buffer, sizeof(buffer) - 1)) { if (Bl0906Checksum(address, &buffer) == buffer.checksum) { Bl0906ube24_t data_u24; Bl0906sbe24_t data_s24; bool signed_result = reference == BL0906_TREF || reference == BL0906_WATT || reference == BL0906_PREF; if (signed_result) { data_s24.l = buffer.l; data_s24.m = buffer.m; data_s24.h = buffer.h; } else { data_u24.l = buffer.l; data_u24.m = buffer.m; data_u24.h = buffer.h; } float value = 0; // Power if (reference == BL0906_PREF) { value = (float) Bl0906_to_int32_t(data_s24) * reference; } // Total power if (reference == BL0906_WATT) { value = (float) Bl0906_to_int32_t(data_s24) * reference; } // Voltage, current, power, total power if (reference == BL0906_UREF || reference == BL0906_IREF || reference == BL0906_EREF || reference == BL0906_CF) { value = (float) Bl0906_to_uint32_t(data_u24) * reference; } // Frequency if (reference == BL0906_FREF) { value = reference / (float) Bl0906_to_uint32_t(data_u24); } // Chip temperature if (reference == BL0906_TREF) { value = (float) Bl0906_to_int32_t(data_s24); value = (value - 64) * 12.5 / 59 - 40; // Celsius // value = (value - 64) * reference; value = ConvertTemp(value); } *sensor = value; } else { AddLog(LOG_LEVEL_DEBUG, PSTR("BL6: CRC error")); while (Bl0906Serial->read() >= 0); // Flush receive buffer } } } /********************************************************************************************/ void Bl0906Loop(void) { if (UINT8_MAX == Bl0906.current_channel) { return; } while (Bl0906Serial->available()) { Bl0906Serial->flush(); } if (0 == Bl0906.current_channel) { #ifdef DEBUG_BL0906 AddLog(LOG_LEVEL_DEBUG, PSTR("BL6: Start polling")); #endif // DEBUG_BL0906 // Temperature Bl0906ReadData(BL0906_TEMPERATURE, BL0906_TREF, &Bl0906.temperature); } else if (1 == Bl0906.current_channel) { Bl0906ReadData(BL0906_I_1_RMS, BL0906_IREF, &Energy->current[0]); Bl0906ReadData(BL0906_WATT_1, BL0906_PREF, &Energy->active_power[0]); Bl0906ReadData(BL0906_CF_1_CNT, BL0906_EREF, &Energy->import_active[0]); Energy->data_valid[0] = 0; } else if (2 == Bl0906.current_channel) { Bl0906ReadData(BL0906_I_2_RMS, BL0906_IREF, &Energy->current[1]); Bl0906ReadData(BL0906_WATT_2, BL0906_PREF, &Energy->active_power[1]); Bl0906ReadData(BL0906_CF_2_CNT, BL0906_EREF, &Energy->import_active[1]); Energy->data_valid[1] = 0; } else if (3 == Bl0906.current_channel) { Bl0906ReadData(BL0906_I_3_RMS, BL0906_IREF, &Energy->current[2]); Bl0906ReadData(BL0906_WATT_3, BL0906_PREF, &Energy->active_power[2]); Bl0906ReadData(BL0906_CF_3_CNT, BL0906_EREF, &Energy->import_active[2]); Energy->data_valid[2] = 0; } else if (4 == Bl0906.current_channel) { Bl0906ReadData(BL0906_I_4_RMS, BL0906_IREF, &Energy->current[3]); Bl0906ReadData(BL0906_WATT_4, BL0906_PREF, &Energy->active_power[3]); Bl0906ReadData(BL0906_CF_4_CNT, BL0906_EREF, &Energy->import_active[3]); Energy->data_valid[3] = 0; } else if (5 == Bl0906.current_channel) { Bl0906ReadData(BL0906_I_5_RMS, BL0906_IREF, &Energy->current[4]); Bl0906ReadData(BL0906_WATT_5, BL0906_PREF, &Energy->active_power[4]); Bl0906ReadData(BL0906_CF_5_CNT, BL0906_EREF, &Energy->import_active[4]); Energy->data_valid[4] = 0; } else if (6 == Bl0906.current_channel) { Bl0906ReadData(BL0906_I_6_RMS, BL0906_IREF, &Energy->current[5]); Bl0906ReadData(BL0906_WATT_6, BL0906_PREF, &Energy->active_power[5]); Bl0906ReadData(BL0906_CF_6_CNT, BL0906_EREF, &Energy->import_active[5]); Energy->data_valid[5] = 0; } else if (8 == Bl0906.current_channel) { // Frequency Bl0906ReadData(BL0906_FREQUENCY, BL0906_FREF, &Energy->frequency[0]); // Voltage Bl0906ReadData(BL0906_V_RMS, BL0906_UREF, &Energy->voltage[0]); } else if (9 == Bl0906.current_channel) { // Total power // Bl0906ReadData(BL0906_WATT_SUM, BL0906_WATT, this->total_power_sensor_); // Total Energy // Bl0906ReadData(BL0906_CF_SUM_CNT, BL0906_CF, &Energy->total[0]); EnergyUpdateTotal(); } else { Bl0906.current_channel = UINT8_MAX - 1; // Stop #ifdef DEBUG_BL0906 AddLog(LOG_LEVEL_DEBUG, PSTR("BL6: Stop polling")); #endif // DEBUG_BL0906 } if (Bl0906.current_channel == Energy->phase_count) { Bl0906.current_channel = 7; // Skip next phases and go to frequency and voltage } Bl0906.current_channel++; /* while (Bl0906Serial->available()) { Bl0906Serial->read(); } */ while (Bl0906Serial->read() >= 0); } void Bl0906EverySecond(void) { if (!(TasmotaGlobal.uptime % BL0906_UPDATE)) { // Every BL0906_UPDATE seconds an update if (UINT8_MAX == Bl0906.current_channel) { Bl0906.current_channel = 0; } } } void Bl0906Show(bool json) { if (json) { ResponseAppend_P(JSON_SNS_F_TEMP, "BL0906", Settings->flag2.temperature_resolution, &Bl0906.temperature); if (0 == TasmotaGlobal.tele_period) { #ifdef USE_DOMOTICZ DomoticzFloatSensor(DZ_TEMP, Bl0906.temperature); #endif // USE_DOMOTICZ #ifdef USE_KNX KnxSensor(KNX_TEMPERATURE, Bl0906.temperature); #endif // USE_KNX } #ifdef USE_WEBSERVER } else { WSContentSend_Temp("", Bl0906.temperature); #endif // USE_WEBSERVER } } void Bl0906Init(void) { // Software serial init needs to be done here as earlier (serial) interrupts may lead to Exceptions Bl0906Serial = new TasmotaSerial(Bl0906.rx_pin, Pin(GPIO_TXD), 1); if (Bl0906Serial->begin(Bl0906.baudrate)) { AddLog(LOG_LEVEL_DEBUG, PSTR("BL6: Serial UART%d"), Bl0906Serial->getUart()); Bl0906Setup(); // Bl0906ResetEnergy(); } } void Bl0906PreInit(void) { if (PinUsed(GPIO_TXD) && PinUsed(GPIO_BL0906_RX, GPIO_ANY)) { Bl0906.rx_pin = Pin(GPIO_BL0906_RX, GPIO_ANY); uint32_t option = GetPin(Bl0906.rx_pin) - AGPIO(GPIO_BL0906_RX); // 0 .. 5 Bl0906.baudrate = 19200; Energy->voltage_common = true; // Use common voltage Energy->frequency_common = true; // Use common frequency Energy->use_overtemp = true; // Use global temperature for overtemp detection Energy->phase_count = option +1; // Handle 1 to 6 channels as phases TasmotaGlobal.energy_driver = XNRG_24; } } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xnrg24(uint32_t function) { bool result = false; switch (function) { case FUNC_LOOP: if (Bl0906Serial) { Bl0906Loop(); } break; case FUNC_EVERY_SECOND: Bl0906EverySecond(); break; case FUNC_JSON_APPEND: Bl0906Show(1); break; #ifdef USE_WEBSERVER case FUNC_WEB_SENSOR: Bl0906Show(0); break; #endif // USE_WEBSERVER case FUNC_INIT: Bl0906Init(); break; case FUNC_PRE_INIT: Bl0906PreInit(); break; } return result; } #endif // USE_BL0906 #endif // USE_ENERGY_SENSOR #endif // ESP32