Tasmota/tasmota/tasmota_xnrg_energy/xnrg_24_bl0906.ino

539 lines
19 KiB
C++

/*
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 <phases>` - Change default 4 column GUI display to <phases> 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.h>
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