/*
xnrg_08_sdm120.ino - Eastron SDM120-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_SDM120
/*********************************************************************************************\
* Eastron SDM120 or SDM220 Modbus energy meter
*
* Based on: https://github.com/reaper7/SDM_Energy_Meter
\*********************************************************************************************/
#define XNRG_08 8
// can be user defined in my_user_config.h
#ifndef SDM120_SPEED
#define SDM120_SPEED 2400 // default SDM120 Modbus baud rate
#endif
// can be user defined in my_user_config.h
#ifndef SDM120_ADDR
#define SDM120_ADDR 1 // default SDM120 Modbus address
#endif
#include
TasmotaModbus *Sdm120Modbus;
const uint8_t sdm120_table = 8;
const uint8_t sdm220_table = 13;
const uint16_t sdm120_start_addresses[] {
0x0000, // SDM120C_VOLTAGE [V]
0x0006, // SDM120C_CURRENT [A]
0x000C, // SDM120C_POWER [W]
0x0012, // SDM120C_APPARENT_POWER [VA]
0x0018, // SDM120C_REACTIVE_POWER [VAR]
0x001E, // SDM120C_POWER_FACTOR
0x0046, // SDM120C_FREQUENCY [Hz]
0x0156, // SDM120C_TOTAL_ACTIVE_ENERGY [kWh]
0X0048, // SDM220_IMPORT_ACTIVE [kWh]
0X004A, // SDM220_EXPORT_ACTIVE [kWh]
0X004C, // SDM220_IMPORT_REACTIVE [kVArh]
0X004E, // SDM220_EXPORT_REACTIVE [kVArh]
0X0024 // SDM220_PHASE_ANGLE [Degree]
};
struct SDM120 {
float total_active = 0;
float import_active = NAN;
float import_reactive = 0;
float export_reactive = 0;
float phase_angle = 0;
uint8_t read_state = 0;
uint8_t send_retry = 0;
uint8_t start_address_count = sdm220_table;
} Sdm120;
/*********************************************************************************************/
void SDM120Every250ms(void)
{
bool data_ready = Sdm120Modbus->ReceiveReady();
if (data_ready) {
uint8_t buffer[14]; // At least 5 + (2 * 2) = 9
uint32_t error = Sdm120Modbus->ReceiveBuffer(buffer, 2);
AddLogBuffer(LOG_LEVEL_DEBUG_MORE, buffer, Sdm120Modbus->ReceiveCount());
if (error) {
AddLog(LOG_LEVEL_DEBUG, PSTR("SDM: SDM120 error %d"), error);
} else {
Energy.data_valid[0] = 0;
// 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];
switch(Sdm120.read_state) {
case 0:
Energy.voltage[0] = value; // 230.2 V
break;
case 1:
Energy.current[0] = value; // 1.260 A
break;
case 2:
Energy.active_power[0] = value; // -196.3 W
break;
case 3:
Energy.apparent_power[0] = value; // 223.4 VA
break;
case 4:
Energy.reactive_power[0] = value; // 92.2
break;
case 5:
Energy.power_factor[0] = value; // -0.91
break;
case 6:
Energy.frequency[0] = value; // 50.0 Hz
break;
case 7:
Sdm120.total_active = value; // 484.708 kWh = import_active + export_active
break;
case 8:
Sdm120.import_active = value; // 478.492 kWh
break;
case 9:
Energy.export_active[0] = value; // 6.216 kWh
break;
case 10:
Sdm120.import_reactive = value; // 172.750 kVArh
break;
case 11:
Sdm120.export_reactive = value; // 2.844 kVArh
break;
case 12:
Sdm120.phase_angle = value; // 0.00 Deg
break;
}
Sdm120.read_state++;
if (Sdm120.read_state == Sdm120.start_address_count) {
Sdm120.read_state = 0;
if (Sdm120.start_address_count > sdm120_table) {
if (!isnan(Sdm120.import_active)) {
Sdm120.total_active = Sdm120.import_active;
} else {
Sdm120.start_address_count = sdm120_table; // No extended registers available
}
}
Energy.import_active[0] = Sdm120.total_active; // 484.708 kWh
EnergyUpdateTotal(); // 484.708 kWh
}
}
} // end data ready
if (0 == Sdm120.send_retry || data_ready) {
Sdm120.send_retry = 5;
Sdm120Modbus->Send(SDM120_ADDR, 0x04, sdm120_start_addresses[Sdm120.read_state], 2);
} else {
Sdm120.send_retry--;
}
}
void Sdm120SnsInit(void)
{
Sdm120Modbus = new TasmotaModbus(Pin(GPIO_SDM120_RX), Pin(GPIO_SDM120_TX));
uint8_t result = Sdm120Modbus->Begin(SDM120_SPEED);
if (result) {
if (2 == result) { ClaimSerial(); }
} else {
TasmotaGlobal.energy_driver = ENERGY_NONE;
}
}
void Sdm120DrvInit(void)
{
if (PinUsed(GPIO_SDM120_RX) && PinUsed(GPIO_SDM120_TX)) {
TasmotaGlobal.energy_driver = XNRG_08;
}
}
void Sdm220Reset(void)
{
if (isnan(Sdm120.import_active)) { return; }
Sdm120.import_active = 0;
Sdm120.import_reactive = 0;
Sdm120.export_reactive = 0;
Sdm120.phase_angle = 0;
}
#ifdef USE_WEBSERVER
const char HTTP_ENERGY_SDM220[] PROGMEM =
"{s}" D_IMPORT_REACTIVE "{m}%s " D_UNIT_KWARH "{e}"
"{s}" D_EXPORT_REACTIVE "{m}%s " D_UNIT_KWARH "{e}"
"{s}" D_PHASE_ANGLE "{m}%s " D_UNIT_ANGLE "{e}";
#endif // USE_WEBSERVER
/*
void Sdm220Show(bool json) {
if (isnan(Sdm120.import_active)) { return; }
char import_active_chr[FLOATSZ];
dtostrfd(Sdm120.import_active, Settings->flag2.energy_resolution, import_active_chr);
char import_reactive_chr[FLOATSZ];
dtostrfd(Sdm120.import_reactive, Settings->flag2.energy_resolution, import_reactive_chr);
char export_reactive_chr[FLOATSZ];
dtostrfd(Sdm120.export_reactive, Settings->flag2.energy_resolution, export_reactive_chr);
char phase_angle_chr[FLOATSZ];
dtostrfd(Sdm120.phase_angle, 2, phase_angle_chr);
if (json) {
ResponseAppend_P(PSTR(",\"" D_JSON_IMPORT_ACTIVE "\":%s,\"" D_JSON_IMPORT_REACTIVE "\":%s,\"" D_JSON_EXPORT_REACTIVE "\":%s,\"" D_JSON_PHASE_ANGLE "\":%s"),
import_active_chr, import_reactive_chr, export_reactive_chr, phase_angle_chr);
#ifdef USE_WEBSERVER
} else {
WSContentSend_PD(HTTP_ENERGY_SDM220, import_reactive_chr, export_reactive_chr, phase_angle_chr);
#endif // USE_WEBSERVER
}
}
*/
void Sdm220Show(bool json) {
if (isnan(Sdm120.import_active)) { return; }
char value_chr[TOPSZ];
char value2_chr[TOPSZ];
char value3_chr[TOPSZ];
char value4_chr[TOPSZ];
if (json) {
ResponseAppend_P(PSTR(",\"" D_JSON_IMPORT_ACTIVE "\":%s,\"" D_JSON_IMPORT_REACTIVE "\":%s,\"" D_JSON_EXPORT_REACTIVE "\":%s,\"" D_JSON_PHASE_ANGLE "\":%s"),
EnergyFormat(value_chr, &Sdm120.import_active, Settings->flag2.energy_resolution),
EnergyFormat(value2_chr, &Sdm120.import_reactive, Settings->flag2.energy_resolution),
EnergyFormat(value3_chr, &Sdm120.export_reactive, Settings->flag2.energy_resolution),
EnergyFormat(value4_chr, &Sdm120.phase_angle, 2));
#ifdef USE_WEBSERVER
} else {
WSContentSend_PD(HTTP_ENERGY_SDM220, WebEnergyFormat(value_chr, &Sdm120.import_reactive, Settings->flag2.energy_resolution, 2),
WebEnergyFormat(value2_chr, &Sdm120.export_reactive, Settings->flag2.energy_resolution, 2),
WebEnergyFormat(value3_chr, &Sdm120.phase_angle, 2));
#endif // USE_WEBSERVER
}
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
bool Xnrg08(uint8_t function)
{
bool result = false;
switch (function) {
case FUNC_EVERY_250_MSECOND:
SDM120Every250ms();
break;
case FUNC_JSON_APPEND:
Sdm220Show(1);
break;
#ifdef USE_WEBSERVER
#ifdef USE_ENERGY_COLUMN_GUI
case FUNC_WEB_COL_SENSOR:
Sdm220Show(0);
break;
#else // not USE_ENERGY_COLUMN_GUI
case FUNC_WEB_SENSOR:
Sdm220Show(0);
break;
#endif // USE_ENERGY_COLUMN_GUI
#endif // USE_WEBSERVER
case FUNC_ENERGY_RESET:
Sdm220Reset();
break;
case FUNC_INIT:
Sdm120SnsInit();
break;
case FUNC_PRE_INIT:
Sdm120DrvInit();
break;
}
return result;
}
#endif // USE_SDM120
#endif // USE_ENERGY_SENSOR