/* xnrg_13_fif_le01mr.ino - F&F LE-01MR energy meter with Modbus interface - support for Tasmota Copyright (C) 2021 Przemyslaw Wistuba 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_LE01MR /*********************************************************************************************\ * F&F LE-01MR - This is a single phase energy meter with rs485 modbus interface * (and bidirectional energy counting - enabled by RS485). * It measure: Active energy imported AE+ [kWh] , Reactive energy imported RE+ [kvarh], * Voltage V [V], Current I [A], Frequency F [Hz], power factor (aka "cos-phi"), * Active power P [kW], Reactive power Q [kVAr], Apparent power S [kVA], * *Active energy exported AE- [kWh] (when meter is switched to bi-directional counting then * reactive energy imported register contains value of Active energy exported). * * Meter descriptions at manufacturer page (english version have some description errors): * EN: https://www.fif.com.pl/en/usage-electric-power-meters/517-electricity-consumption-meter-le-01mr.html * PL: https://www.fif.com.pl/pl/liczniki-zuzycia-energii-elektrycznej/517-licznik-zuzycia-energii-le-01mr.html * * Note about communication settings: The meter must be reconfigured to use baudrate 2400 (or 9600) *without* * parity bit - by default the meter is configured to 9600 8E1 * (Frame format: "EVEN 1") . To make those changes, use LE-Config * software (can be found in download tab in product page - link above) * and USB-RS485 dongle (those cheap ~2$ from ali works fine) * * Register descriptions (not all, only those that are being read): * * /----------------------------------- Register address * | /-------------------------- Registers count * | | /---------------------- Datatype and size * | | | /----------------- Resolution (or multiplier) * | | | | /---------- Unit * | | | | | /---- Description * 0x0130 1 U16 0.01 Hz Frequency * 0x0131 1 U16 0.01 V Voltage * 0x0139 2 U32 0.001 A Current * 0x0140 2 U32 0.001 kW Active power * 0x0148 2 U32 0.001 kvar Reactive power * 0x0150 2 U32 0.001 kVA Apparent power * 0x0158 1 S16 0.001 - Power factor * 0xA000 2 U32 0.01 kWh Active energy imported * 0xA01E 2 U32 0.01 kvarh Reactive energy imported * * Datatype: S = signed int, U = unsigend int, * U32 - the first (lower) register contains high word, * second register contains lower word of 32bit dword: * value_32bit = (register+0)<<16 | (register+1); * /or/ val32bit = (reg+0)*65536 + (reg+1); * * Note about MQTT/JSON: In fields "ENERGY.TotalActive" and "ENERGY.TotalReactive" there are * counters values directly from the meter (without Tasmota calculation, * energy used calculated by Tasmota is in Total/Today fields ). * Filed "ENERGY.Period" is always zero. \*********************************************************************************************/ #define XNRG_13 13 // can be user defined in my_user_config.h #ifndef LE01MR_SPEED #define LE01MR_SPEED 2400 // default LE01MR Modbus speed #endif // can be user defined in my_user_config.h #ifndef LE01MR_ADDR #define LE01MR_ADDR 1 // default LE01MR Modbus address #endif #include TasmotaModbus *FifLEModbus; const uint8_t le01mr_table_sz = 9; const uint16_t le01mr_register_addresses[] { // IDX (reg count/datatype) [unit] 0x0130, // 00 . LE01MR_FREQUENCY (1/U16) [Hz] 0x0131, // 01 . LE01MR_VOLTAGE (1/U16) [V] 0x0158, // 02 . LE01MR_POWER_FACTOR (1/S16) 0x0139, // 03 . LE01MR_CURRENT (2/U32) [A] 0x0140, // 04 . LE01MR_ACTIVE_POWER (2/U32) [kW] 0x0148, // 05 . LE01MR_REACTIVE_POWER (2/U32) [kvar] 0x0150, // 06 . LE01MR_APPARENT_POWER (2/U32) [kVA] 0xA000, // 07 . LE01MR_TOTAL_ACTIVE_ENERGY (2/U32) [kWh] 0xA01E // 08 . LE01MR_TOTAL_REACTIVE_ENERGY (2/U32) [kvarh] }; struct LE01MR { float total_active = 0; float total_reactive = 0; uint8_t read_state = 0; uint8_t send_retry = 0; uint8_t start_address_count = le01mr_table_sz; } Le01mr; /*********************************************************************************************/ void FifLEEvery250ms(void) { bool data_ready = FifLEModbus->ReceiveReady(); if (data_ready) { uint8_t buffer[14]; // At least 9 uint8_t reg_count = 2; if (Le01mr.read_state < 3) { reg_count=1; } uint32_t error = FifLEModbus->ReceiveBuffer(buffer, reg_count); AddLogBuffer(LOG_LEVEL_DEBUG_MORE, buffer, FifLEModbus->ReceiveCount()); if (error) { AddLog(LOG_LEVEL_DEBUG, PSTR("FiF-LE: LE01MR Modbus error %d"), error); } else { Energy.data_valid[0] = 0; // CA=Client Address, FC=Function Code, BC=Byte Count, B3..B0=Data byte, Ch Cl = crc16 checksum // U32 registers: // 00 01 02 03 04 05 06 07 08 // CA FC BC B3 B2 B1 B0 Cl Ch // 01 03 04 00 00 00 72 7A 16 = REG[B3..B2=0x0139,B1..B0=0x013A] 114 = 0.114 A // 01 03 04 00 00 00 B0 FB 87 = REG[B3..B2=0xA01E,B1..B0=0xA01F] 176 = 1.76 kvarh // U16/S16 registers: // 00 01 02 03 04 05 06 // CA FC BC B1 B0 Cl Ch // 01 03 02 5B 02 02 B5 = REG[B1..B0=0x0131] 23298 = 232.98 V // 01 03 02 03 E8 B8 FA = REG[B1..B0=0x0158] 1000 = 1.000 (power factor) // there are 3 data types used: // U16 - uint16_t // U32 - uint32_t // S16 - int16_t // everything drop into uint32 value, but depending on register ther will be 2 or 4 bytes uint32_t value_buff = 0; // for register table items 0..2 use 2 bytes (U16) if (Le01mr.read_state >= 0 && Le01mr.read_state < 3) { // value_buff = ((uint32_t)buffer[3])<<8 | buffer[4]; } else { value_buff = ((uint32_t)buffer[3])<<24 | ((uint32_t)buffer[4])<<16 | ((uint32_t)buffer[5])<<8 | buffer[6]; } switch(Le01mr.read_state) { case 0: Energy.frequency[0] = value_buff * 0.01f; // 5000 => 50.00 break; case 1: Energy.voltage[0] = value_buff * 0.01f; // 23298 => 232.98 V break; case 2: Energy.power_factor[0] = ((int16_t)value_buff) * 0.001f; // 1000 => 1.000 //note: I never saw this negative... break; case 3: Energy.current[0] = value_buff * 0.001f; // 114 => 0.114 A break; case 4: Energy.active_power[0] = value_buff * 1.0f; // P [W] break; case 5: Energy.reactive_power[0] = value_buff * 1.0f; // Q [var] break; case 6: Energy.apparent_power[0] = value_buff * 1.0f; // S [VA] break; case 7: Energy.import_active[0] = value_buff * 0.01f; // [kWh] Le01mr.total_active = Energy.import_active[0]; // Useless break; case 8: Le01mr.total_reactive = value_buff * 0.01f; // [kvarh] 176 => 1.76 break; } Le01mr.read_state++; if (Le01mr.read_state == Le01mr.start_address_count) { Le01mr.read_state = 0; EnergyUpdateTotal(); } } } // end data ready if (0 == Le01mr.send_retry || data_ready) { uint8_t reg_count = 2; Le01mr.send_retry = 5; // some registers are 1reg in size if (Le01mr.read_state < 3) reg_count=1; // send request FifLEModbus->Send(LE01MR_ADDR, 0x03, le01mr_register_addresses[Le01mr.read_state], reg_count); } else { Le01mr.send_retry--; } } void FifLESnsInit(void) { FifLEModbus = new TasmotaModbus(Pin(GPIO_LE01MR_RX), Pin(GPIO_LE01MR_TX)); uint8_t result = FifLEModbus->Begin(LE01MR_SPEED); if (result) { if (2 == result) { ClaimSerial(); } } else { TasmotaGlobal.energy_driver = ENERGY_NONE; } } void FifLEDrvInit(void) { if (PinUsed(GPIO_LE01MR_RX) && PinUsed(GPIO_LE01MR_TX)) { TasmotaGlobal.energy_driver = XNRG_13; } } void FifLEReset(void) { Le01mr.total_active = 0; Le01mr.total_reactive = 0; } #ifdef USE_WEBSERVER const char HTTP_ENERGY_LE01MR[] PROGMEM = "{s}" D_TOTAL_ACTIVE "{m}%s " D_UNIT_KILOWATTHOUR "{e}" "{s}" D_TOTAL_REACTIVE "{m}%s " D_UNIT_KWARH "{e}" ; #endif // USE_WEBSERVER /* void FifLEShow(bool json) { char total_reactive_chr[FLOATSZ]; dtostrfd(Le01mr.total_reactive, Settings->flag2.energy_resolution, total_reactive_chr); char total_active_chr[FLOATSZ]; dtostrfd(Le01mr.total_active, Settings->flag2.energy_resolution, total_active_chr); if (json) { ResponseAppend_P(PSTR(",\"" D_JSON_TOTAL_ACTIVE "\":%s,\"" D_JSON_TOTAL_REACTIVE "\":%s"), total_active_chr, total_reactive_chr); #ifdef USE_WEBSERVER } else { WSContentSend_PD(HTTP_ENERGY_LE01MR, total_active_chr, total_reactive_chr); #endif // USE_WEBSERVER } } */ void FifLEShow(bool json) { char value_chr[GUISZ]; char value2_chr[GUISZ]; if (json) { ResponseAppend_P(PSTR(",\"" D_JSON_TOTAL_ACTIVE "\":%s,\"" D_JSON_TOTAL_REACTIVE "\":%s"), EnergyFormat(value_chr, &Le01mr.total_active, Settings->flag2.energy_resolution), EnergyFormat(value2_chr, &Le01mr.total_reactive, Settings->flag2.energy_resolution)); #ifdef USE_WEBSERVER } else { WSContentSend_PD(HTTP_ENERGY_LE01MR, WebEnergyFormat(value_chr, &Le01mr.total_active, Settings->flag2.energy_resolution), WebEnergyFormat(value2_chr, &Le01mr.total_reactive, Settings->flag2.energy_resolution)); #endif // USE_WEBSERVER } } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xnrg13(uint8_t function) { bool result = false; switch (function) { case FUNC_EVERY_250_MSECOND: FifLEEvery250ms(); break; case FUNC_JSON_APPEND: FifLEShow(1); break; #ifdef USE_WEBSERVER #ifdef USE_ENERGY_COLUMN_GUI case FUNC_WEB_COL_SENSOR: #else // not USE_ENERGY_COLUMN_GUI case FUNC_WEB_SENSOR: #endif // USE_ENERGY_COLUMN_GUI FifLEShow(0); break; #endif // USE_WEBSERVER case FUNC_ENERGY_RESET: FifLEReset(); break; case FUNC_INIT: FifLESnsInit(); break; case FUNC_PRE_INIT: FifLEDrvInit(); break; } return result; } #endif // USE_LE01MR #endif // USE_ENERGY_SENSOR