From f4b5e565ef5f3409f6fd1a2cdc943ee1b7cf5d50 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Sun, 1 Sep 2019 17:51:25 +0200 Subject: [PATCH] Add driver USE_SDM120_2 with Domoticz P1 Smart Meter functionality as future replacement for USE_SDM120 - Pls test and report Add driver USE_SDM120_2 with Domoticz P1 Smart Meter functionality as future replacement for USE_SDM120 - Pls test and report (#6282) --- sonoff/_changelog.ino | 1 + sonoff/my_user_config.h | 3 +- sonoff/sonoff.h | 3 +- sonoff/xdrv_03_energy.ino | 42 +++--- sonoff/xnrg_09_sdm120.ino | 293 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 322 insertions(+), 20 deletions(-) create mode 100644 sonoff/xnrg_09_sdm120.ino diff --git a/sonoff/_changelog.ino b/sonoff/_changelog.ino index 11520b0a2..06bd9d956 100644 --- a/sonoff/_changelog.ino +++ b/sonoff/_changelog.ino @@ -5,6 +5,7 @@ * Add compile time define USE_WS2812_HARDWARE to select hardware type WS2812, WS2812X, WS2813, SK6812, LC8812 or APA106 (DMA mode only) * Add 'sonoff-ir' pre-packaged IR-dedicated firmware and 'sonoff-ircustom' to customize firmware with IR Full protocol support * Add Zigbee support phase 2 - cc2530 initialization and basic ZCL decoding + * Add driver USE_SDM120_2 with Domoticz P1 Smart Meter functionality as future replacement for USE_SDM120 - Pls test and report * * 6.6.0.8 20190827 * Add Tuya Energy monitoring by Shantur Rathore diff --git a/sonoff/my_user_config.h b/sonoff/my_user_config.h index 3ae3d9574..f55fc774f 100644 --- a/sonoff/my_user_config.h +++ b/sonoff/my_user_config.h @@ -429,8 +429,9 @@ #define USE_PZEM_AC // Add support for PZEM014,016 Energy monitor (+1k1 code) #define USE_PZEM_DC // Add support for PZEM003,017 Energy monitor (+1k1 code) #define USE_MCP39F501 // Add support for MCP39F501 Energy monitor as used in Shelly 2 (+3k1 code) +//#define USE_SDM120_2 // Add support for Eastron SDM120-Modbus energy meter (+1k4 code) -//#define USE_SDM120 // Add support for Eastron SDM120-Modbus energy meter (+1k7 code) +//#define USE_SDM120 // Add support for Eastron SDM120-Modbus energy meter (+2k4 code) #define SDM120_SPEED 2400 // SDM120-Modbus RS485 serial speed (default: 2400 baud) #define USE_SDM220 // Add extra parameters for SDM220 (+0k1 code) //#define USE_SDM630 // Add support for Eastron SDM630-Modbus energy meter (+2k code) diff --git a/sonoff/sonoff.h b/sonoff/sonoff.h index 1ce925355..6b0c52ce4 100644 --- a/sonoff/sonoff.h +++ b/sonoff/sonoff.h @@ -113,6 +113,7 @@ const uint16_t SERIALLOG_TIMER = 600; // Seconds to disable SerialLog const uint8_t OTA_ATTEMPTS = 5; // Number of times to try fetching the new firmware const uint16_t INPUT_BUFFER_SIZE = 520; // Max number of characters in (serial and http) command buffer +const uint16_t FLOATSZ = 33; // Max number of characters in float result from dtostrfd const uint16_t CMDSZ = 24; // Max number of characters in command const uint16_t TOPSZ = 100; // Max number of characters in topic string const uint16_t LOGSZ = 520; // Max number of characters in log @@ -261,7 +262,7 @@ enum XsnsFunctions {FUNC_SETTINGS_OVERRIDE, FUNC_PIN_STATE, FUNC_MODULE_INIT, FU FUNC_PREP_BEFORE_TELEPERIOD, FUNC_JSON_APPEND, FUNC_WEB_SENSOR, FUNC_COMMAND, FUNC_COMMAND_SENSOR, FUNC_COMMAND_DRIVER, FUNC_MQTT_SUBSCRIBE, FUNC_MQTT_INIT, FUNC_MQTT_DATA, FUNC_SET_POWER, FUNC_SET_DEVICE_POWER, FUNC_SHOW_SENSOR, - FUNC_ENERGY_EVERY_SECOND, + FUNC_ENERGY_EVERY_SECOND, FUNC_ENERGY_RESET, FUNC_RULES_PROCESS, FUNC_SERIAL, FUNC_FREE_MEM, FUNC_BUTTON_PRESSED, FUNC_WEB_ADD_BUTTON, FUNC_WEB_ADD_MAIN_BUTTON, FUNC_WEB_ADD_HANDLER, FUNC_SET_CHANNELS}; diff --git a/sonoff/xdrv_03_energy.ino b/sonoff/xdrv_03_energy.ino index bf8e9afdc..2f8561f42 100644 --- a/sonoff/xdrv_03_energy.ino +++ b/sonoff/xdrv_03_energy.ino @@ -76,6 +76,7 @@ struct ENERGY { float reactive_power = NAN; // 123.1 VAr float power_factor = NAN; // 0.12 float frequency = NAN; // 123.1 Hz + float start_energy = 0; // 12345.12345 kWh total previous float daily = 0; // 123.123 kWh @@ -114,7 +115,6 @@ struct ENERGY { uint8_t mplr_counter = 0; uint8_t max_energy_state = 0; #endif // USE_ENERGY_POWER_LIMIT - #endif // USE_ENERGY_MARGIN_DETECTION } Energy; @@ -384,9 +384,14 @@ void EnergyOverTempCheck() Energy.voltage = 0; Energy.current = 0; Energy.active_power = 0; + if (!isnan(Energy.apparent_power)) { Energy.apparent_power = 0; } + if (!isnan(Energy.reactive_power)) { Energy.reactive_power = 0; } if (!isnan(Energy.frequency)) { Energy.frequency = 0; } if (!isnan(Energy.power_factor)) { Energy.power_factor = 0; } Energy.start_energy = 0; + + XnrgCall(FUNC_ENERGY_RESET); + } } } @@ -450,11 +455,11 @@ void CmndEnergyReset(void) RtcSettings.energy_usage.usage1_kWhtotal = Settings.energy_kWhtotal; } - char energy_total_chr[33]; + char energy_total_chr[FLOATSZ]; dtostrfd(Energy.total, Settings.flag2.energy_resolution, energy_total_chr); - char energy_daily_chr[33]; + char energy_daily_chr[FLOATSZ]; dtostrfd(Energy.daily, Settings.flag2.energy_resolution, energy_daily_chr); - char energy_yesterday_chr[33]; + char energy_yesterday_chr[FLOATSZ]; dtostrfd((float)Settings.energy_kWhyesterday / 100000, Settings.flag2.energy_resolution, energy_yesterday_chr); Response_P(PSTR("{\"%s\":{\"" D_JSON_TOTAL "\":%s,\"" D_JSON_YESTERDAY "\":%s,\"" D_JSON_TODAY "\":%s}}"), @@ -696,7 +701,7 @@ const char HTTP_ENERGY_SNS1[] PROGMEM = "{s}" D_POWER_FACTOR "{m}%s{e}"; const char HTTP_ENERGY_SNS2[] PROGMEM = - "{s}" D_ENERGY_TODAY "{m}%s " D_UNIT_KILOWATTHOUR "{e}" + "{s}" D_ENERGY_TODAY "{m}%s " D_UNIT_KILOWATTHOUR "{e}" "{s}" D_ENERGY_YESTERDAY "{m}%s " D_UNIT_KILOWATTHOUR "{e}" "{s}" D_ENERGY_TOTAL "{m}%s " D_UNIT_KILOWATTHOUR "{e}"; // {s} = , {m} = , {e} = #endif // USE_WEBSERVER @@ -704,16 +709,15 @@ const char HTTP_ENERGY_SNS2[] PROGMEM = void EnergyShow(bool json) { char speriod[20]; -// char sfrequency[20]; bool show_energy_period = (0 == tele_period); float power_factor = Energy.power_factor; - char apparent_power_chr[33]; - char reactive_power_chr[33]; - char power_factor_chr[33]; - char frequency_chr[33]; + char apparent_power_chr[FLOATSZ]; + char reactive_power_chr[FLOATSZ]; + char power_factor_chr[FLOATSZ]; + char frequency_chr[FLOATSZ]; if (!Energy.type_dc) { if (Energy.current_available && Energy.voltage_available) { float apparent_power = Energy.apparent_power; @@ -749,21 +753,21 @@ void EnergyShow(bool json) } } - char voltage_chr[33]; + char voltage_chr[FLOATSZ]; dtostrfd(Energy.voltage, Settings.flag2.voltage_resolution, voltage_chr); - char current_chr[33]; + char current_chr[FLOATSZ]; dtostrfd(Energy.current, Settings.flag2.current_resolution, current_chr); - char active_power_chr[33]; + char active_power_chr[FLOATSZ]; dtostrfd(Energy.active_power, Settings.flag2.wattage_resolution, active_power_chr); - char energy_daily_chr[33]; + char energy_daily_chr[FLOATSZ]; dtostrfd(Energy.daily, Settings.flag2.energy_resolution, energy_daily_chr); - char energy_yesterday_chr[33]; + char energy_yesterday_chr[FLOATSZ]; dtostrfd((float)Settings.energy_kWhyesterday / 100000, Settings.flag2.energy_resolution, energy_yesterday_chr); - char energy_total_chr[33]; + char energy_total_chr[FLOATSZ]; dtostrfd(Energy.total, Settings.flag2.energy_resolution, energy_total_chr); float energy = 0; - char energy_period_chr[33]; + char energy_period_chr[FLOATSZ]; if (show_energy_period) { if (Energy.period) energy = (float)(Energy.kWhtoday - Energy.period) / 100; Energy.period = Energy.kWhtoday; @@ -789,6 +793,7 @@ void EnergyShow(bool json) if (Energy.current_available) { ResponseAppend_P(PSTR(",\"" D_JSON_CURRENT "\":%s"), current_chr); } + XnrgCall(FUNC_JSON_APPEND); ResponseJsonEnd(); #ifdef USE_DOMOTICZ @@ -797,7 +802,7 @@ void EnergyShow(bool json) DomoticzSensorPowerEnergy((int)Energy.active_power, energy_total_chr); // PowerUsage, EnergyToday dtostrfd((Energy.total - Energy.total1) * 1000, 1, energy_total_chr); // Tariff2 - char energy_total1_chr[33]; + char energy_total1_chr[FLOATSZ]; dtostrfd(Energy.total1 * 1000, 1, energy_total1_chr); // Tariff1 char energy_non[2] = "0"; DomoticzSensorP1SmartMeter(energy_total1_chr, energy_total_chr, energy_non, energy_non, (int)Energy.active_power, 0); @@ -843,6 +848,7 @@ void EnergyShow(bool json) } } WSContentSend_PD(HTTP_ENERGY_SNS2, energy_daily_chr, energy_yesterday_chr, energy_total_chr); + XnrgCall(FUNC_WEB_SENSOR); #endif // USE_WEBSERVER } } diff --git a/sonoff/xnrg_09_sdm120.ino b/sonoff/xnrg_09_sdm120.ino new file mode 100644 index 000000000..e6070911c --- /dev/null +++ b/sonoff/xnrg_09_sdm120.ino @@ -0,0 +1,293 @@ +/* + xnrg_09_sdm120.ino - Eastron SDM120-Modbus energy meter support for Sonoff-Tasmota + + Copyright (C) 2019 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_2 +/*********************************************************************************************\ + * Eastron SDM120 or SDM220 Modbus energy meter + * + * Based on: https://github.com/reaper7/SDM_Energy_Meter +\*********************************************************************************************/ + +#define XNRG_09 9 + +// can be user defined in my_user_config.h +#ifndef SDM120_SPEED + #define SDM120_SPEED 2400 // default SDM120 Modbus address +#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 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] +#ifdef USE_SDM220 + 0x0156, // SDM120C_TOTAL_ACTIVE_ENERGY [Wh] + 0X0024, // SDM220_PHASE_ANGLE [Degre] + 0X0048, // SDM220_IMPORT_ACTIVE [kWh] + 0X004A, // SDM220_EXPORT_ACTIVE [kWh] + 0X004C, // SDM220_IMPORT_REACTIVE [kVArh] + 0X004E, // SDM220_EXPORT_REACTIVE [kVArh] + 0X0158 // SDM220 TOTAL_REACTIVE [kVArh] +#else // USE_SDM220 + 0x0156 // SDM120C_TOTAL_ACTIVE_ENERGY [Wh] +#endif // USE_SDM220 +}; + +struct SDM120 { + uint8_t read_state = 0; + uint8_t send_retry = 0; +} Sdm120; + +#ifdef USE_SDM220 +struct SDM220 { + float phase_angle = 0; + float import_active = 0; + float export_active = 0; + float import_reactive = 0; + float export_reactive = 0; + float total_reactive = 0; +} Sdm220; +#endif // USE_SDM220 + +/*********************************************************************************************/ + +void SDM120Every200ms(void) +{ + bool data_ready = Sdm120Modbus->ReceiveReady(); + + if (data_ready) { + uint8_t buffer[9]; + + uint32_t error = Sdm120Modbus->ReceiveBuffer(buffer, 2); + AddLogBuffer(LOG_LEVEL_DEBUG_MORE, buffer, (buffer[2]) ? buffer[2] +5 : sizeof(buffer)); + + if (error) { + AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "SDM120 response error %d"), error); + } else { + Energy.data_valid = 0; + + float value; + ((uint8_t*)&value)[3] = buffer[3]; + ((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 = value; + break; + + case 1: + Energy.current = value; + break; + + case 2: + Energy.active_power = value; + break; + + case 3: + Energy.apparent_power = value; + break; + + case 4: + Energy.reactive_power = value; + break; + + case 5: + Energy.power_factor = value; + break; + + case 6: + Energy.frequency = value; + break; + + case 7: + if (!Energy.start_energy || (value < Energy.start_energy)) { + Energy.start_energy = value; // Init after restart and hanlde roll-over if any + } + if (value != Energy.start_energy) { + Energy.kWhtoday += (unsigned long)((value - Energy.start_energy) * 100); + Energy.start_energy = value; + } + EnergyUpdateToday(); + break; + +#ifdef USE_SDM220 + case 8: + Sdm220.phase_angle = value; + break; + + case 9: + Sdm220.import_active = value; + break; + + case 10: + Sdm220.export_active = value; + break; + + case 11: + Sdm220.import_reactive = value; + break; + + case 12: + Sdm220.export_reactive = value; + break; + + case 13: + Sdm220.total_reactive = value; + break; +#endif // USE_SDM220 + } // end switch + + Sdm120.read_state++; + if (sizeof(sdm120_start_addresses)/2 == Sdm120.read_state) { + Sdm120.read_state = 0; + } + } + } // 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 { + energy_flg = ENERGY_NONE; + } +} + +void Sdm120DrvInit(void) +{ + if (!energy_flg) { + if ((pin[GPIO_SDM120_RX] < 99) && (pin[GPIO_SDM120_TX] < 99)) { + energy_flg = XNRG_09; + } + } +} + +#ifdef USE_SDM220 + +void Sdm220Reset(void) +{ + Sdm220.phase_angle = 0; + Sdm220.import_active = 0; + Sdm220.export_active = 0; + Sdm220.import_reactive = 0; + Sdm220.export_reactive = 0; + Sdm220.total_reactive = 0; +} + +#ifdef USE_WEBSERVER +const char HTTP_ENERGY_SDM220[] PROGMEM = + "{s}" D_PHASE_ANGLE "{m}%s " D_UNIT_ANGLE "{e}" + "{s}" D_IMPORT_ACTIVE "{m}%s " D_UNIT_KILOWATTHOUR "{e}" + "{s}" D_EXPORT_ACTIVE "{m}%s " D_UNIT_KILOWATTHOUR "{e}" + "{s}" D_IMPORT_REACTIVE "{m}%s " D_UNIT_KWARH "{e}" + "{s}" D_EXPORT_REACTIVE "{m}%s " D_UNIT_KWARH "{e}" + "{s}" D_TOTAL_REACTIVE "{m}%s " D_UNIT_KWARH "{e}"; +#endif // USE_WEBSERVER + +void Sdm220Show(bool json) +{ + char phase_angle_chr[FLOATSZ]; + dtostrfd(Sdm220.phase_angle, 2, phase_angle_chr); + char import_active_chr[FLOATSZ]; + dtostrfd(Sdm220.import_active, Settings.flag2.wattage_resolution, import_active_chr); + char export_active_chr[FLOATSZ]; + dtostrfd(Sdm220.export_active, Settings.flag2.wattage_resolution, export_active_chr); + char import_reactive_chr[FLOATSZ]; + dtostrfd(Sdm220.import_reactive, Settings.flag2.wattage_resolution, import_reactive_chr); + char export_reactive_chr[FLOATSZ]; + dtostrfd(Sdm220.export_reactive, Settings.flag2.wattage_resolution, export_reactive_chr); + char total_reactive_chr[FLOATSZ]; + dtostrfd(Sdm220.total_reactive, Settings.flag2.wattage_resolution, total_reactive_chr); + + if (json) { + ResponseAppend_P(PSTR(",\"" D_JSON_PHASE_ANGLE "\":%s,\"" D_JSON_IMPORT_ACTIVE "\":%s,\"" D_JSON_EXPORT_ACTIVE "\":%s,\"" D_JSON_IMPORT_REACTIVE "\":%s,\"" D_JSON_EXPORT_REACTIVE "\":%s,\"" D_JSON_TOTAL_REACTIVE "\":%s"), + phase_angle_chr, import_active_chr, export_active_chr, import_reactive_chr, export_reactive_chr, total_reactive_chr); +#ifdef USE_WEBSERVER + } else { + WSContentSend_PD(HTTP_ENERGY_SDM220, phase_angle_chr, import_active_chr, export_active_chr, import_reactive_chr, export_reactive_chr, total_reactive_chr); +#endif // USE_WEBSERVER + } +} + +#endif // USE_SDM220 + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +int Xnrg09(uint8_t function) +{ + int result = 0; + + if (FUNC_PRE_INIT == function) { + Sdm120DrvInit(); + } + else if (XNRG_09 == energy_flg) { + switch (function) { + case FUNC_INIT: + Sdm120SnsInit(); + break; + case FUNC_EVERY_200_MSECOND: + if (uptime > 4) { SDM120Every200ms(); } + break; + +#ifdef USE_SDM220 + case FUNC_ENERGY_RESET: + Sdm220Reset(); + break; + case FUNC_JSON_APPEND: + Sdm220Show(1); + break; +#ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: + Sdm220Show(0); + break; +#endif // USE_WEBSERVER +#endif // USE_SDM220 + + } + } + return result; +} + +#endif // USE_SDM120_2 +#endif // USE_ENERGY_SENSOR