├── .gitignore ├── README.md ├── custom_firmware ├── .vscode │ ├── extensions.json │ └── settings.json ├── README.md ├── build │ ├── esp-07.bin │ └── wemos_d1_mini.bin ├── platformio.ini └── src │ ├── Config.cpp │ ├── Config.h │ ├── Debug.h │ ├── InverterValue.cpp │ ├── InverterValue.h │ └── main.cpp ├── esphome_config └── README.md ├── images ├── config.png ├── desk1.jpg ├── desk2.jpg ├── esphome_ha.png ├── inverter1.jpg ├── inverter2.jpg ├── prototype.jpg ├── schematic.jpg └── tasmota.png └── tasmota_smi ├── README.md └── user_config_override.h /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | .DS_Store 7 | .travis.yml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SofarSolar ESP RS485 logger to MQTT 2 | 3 | ## Background 4 | From the shelf logger is only works with SolarMan cloud, but I wanted to have full local integration with Home Assistant, because of poor internet connection. Additionally refresh interval from orginal logger (I managed to integrate with Home Assistant too, see HA Add-One project) is not synchronized with energy meter sensor which checks consumption and production from/to grid. The result was weird, because power wheel shows that house produced energy. 5 | 6 | I asked SofarSolar for documentation of RS485 communication, but no lack. I managed to read register based on old SofarSolar documentation for other inverters models other than KTL-X (my inverter) and reversed engineered data based on information sent to SolarMan cloud. 7 | 8 | At the end I finished with two working loggers in parallel ;-) 9 | 10 | ## Other projects 11 | Here is a list of other project that you may be intrested: 12 | * https://github.com/MichaluxPL/Sofar_LSW3 - uses v5 protocol and directly connect to standard WiFi Logger - very intresting project. 13 | 14 | ## Supported inverters 15 | For sure SofarSolar KTL-X 8.8 and other powers from KTL-X. There is a chance that other brands from the same familly with work, so if someone check please let me know. 16 | 17 | Tested supported devices: 18 | * Sofar KTL-X 8.8 (whole series of KTL-X should work) 19 | * Sofar 4K TLM-G2 20 | 21 | ## Parts needed to build own local logger 22 | * "ESP-07 WiFi serial" - you will find the part under this name 23 | * RS485 to TTL converter compatible with 3v3 (3v3 is an important, because ESP-07 operates on 3v3 TTL levels) 24 | * External antena with connector 25 | * RJ45 connector and cables 26 | 27 | I bought everything from China directly. Total cost is around $5, so much cheaper than orignal logger and more freedom with integration. 28 | 29 | ## Schematic 30 | ![](images/schematic.jpg) 31 | 32 | ## Firmware choice 33 | 34 | My first approach was to write custom firmware, because I couldn't find any other solution, but overtime I realized that there can by some other solutions which can integrate with modbus like Tasmota or ESPHome. With Tasmota and ESPHome the same goal can be achived. In this project you can find all solutions that I tried: 35 | 36 | * [Custom firmware](custom_firmware) 37 | * [Tasmota SMI script](tasmota_smi) 38 | * [ESPHome configuration](esphome_config) 39 | 40 | I prefer ESPHome script, because of seamless integration with ESPHome including correct support for icon and device class. 41 | 42 | Custom software is ok only for my use, because it's dedicated to my setup and my hardware, so I see some issues and it's hard to maintain this solution, comparing to Tasmota and ESPHome supports where there is huge community to support it. 43 | 44 | Tasmota SMI script is not fully tested, but I'm leaving script maybe someone wants to use it for some other integrations for example Domoticz (my goal was HomeAssistant integration) 45 | 46 | ## Images 47 | ### Prototype 48 | ![](images/prototype.jpg) 49 | ### Final solution 50 | ![](images/desk1.jpg) 51 | ![](images/desk2.jpg) 52 | ![](images/inverter1.jpg) 53 | ![](images/inverter2.jpg) 54 | -------------------------------------------------------------------------------- /custom_firmware/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /custom_firmware/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "array": "cpp", 4 | "deque": "cpp", 5 | "list": "cpp", 6 | "string": "cpp", 7 | "unordered_map": "cpp", 8 | "vector": "cpp", 9 | "initializer_list": "cpp", 10 | "sstream": "cpp" 11 | } 12 | } -------------------------------------------------------------------------------- /custom_firmware/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Custom firmware 3 | 4 | ## Image installation 5 | Download image from *build* folder and load it to your ESP. There is a lot of tutorials how to do it on the internet ;-) 6 | 7 | I used esptool: 8 | ``` 9 | $ esptool.py write_flash 0x0 build/esp-07.bin 10 | ``` 11 | 12 | ## Connection to the inverter 13 | Check last image. I used 5V power directly from the inverter. Red cable on photo is VCC and brown is GND. Inverter provides power only during a day, but solutions uses MQTT, so during the night there is no messages. 14 | 15 | ## Configuration 16 | Solution uses IotWebConf library, so after uploading image and switching ESP to boot mode, you should see access point to which you can connect and configure your ESP RS485 logger. 17 | 18 | Default IP: 192.168.4.1 19 | Default WiFi password: "sofarsolar2mqtt" 20 | 21 | After first configuration, you will need login and password to access configuration: 22 | 23 | *Username* is admin 24 | 25 | *Password* is set during first configuration ("AP password") 26 | 27 | 28 | All available parameters are on screen below: 29 | 30 | ![](../images/config.png) 31 | 32 | 33 | ## Sample MQTT message 34 | ``` 35 | { 36 | "status": "normal", 37 | "fault_message": "", 38 | "dc_voltage_1": 324.1, 39 | "dc_current_1": 5.38, 40 | "dc_voltage_2": 252.3, 41 | "dc_current_2": 5.4, 42 | "ac_power": 2950, 43 | "ac_frequency": 49.96, 44 | "ac_voltage_1": 228.3, 45 | "ac_current_1": 4.4, 46 | "ac_voltage_2": 227.9, 47 | "ac_current_2": 4.39, 48 | "ac_voltage_3": 228.5, 49 | "ac_current_3": 4.39, 50 | "energy_total": 4882, 51 | "running_time": 2919, 52 | "energy_today": 2.92, 53 | "temperature_module": 38, 54 | "temperature_inverter": 49, 55 | "bus_voltage": 622.8, 56 | "vice_cpu_input_voltage_1": 323.1, 57 | "countdown_timer": 60, 58 | "pv1_insulation_resistance": 1324, 59 | "pv2_insulation_resistance": 2022, 60 | "isolation_impedance": 1635, 61 | "country_code?": 12, 62 | "phase_a_distribution": 1006, 63 | "phase_b_distribution": 996, 64 | "phase_c_distribution": 975 65 | } 66 | ``` 67 | 68 | ## Sample Home Assistant configuration 69 | Part of the sensors are commented, because they are too technical. 70 | 71 | ``` 72 | sensor sofar: 73 | - platform: mqtt 74 | name: "Inverter status" 75 | state_topic: "inverter/status" 76 | value_template: "{{ value_json.status }}" 77 | - platform: mqtt 78 | name: "Inverter fault message" 79 | state_topic: "inverter/status" 80 | value_template: "{{ value_json.fault_message }}" 81 | - platform: mqtt 82 | name: "Inverter DC voltage 1" 83 | state_topic: "inverter/status" 84 | unit_of_measurement: 'V' 85 | value_template: "{{ value_json.dc_voltage_1 }}" 86 | - platform: mqtt 87 | name: "Inverter DC current 1" 88 | state_topic: "inverter/status" 89 | unit_of_measurement: 'A' 90 | value_template: "{{ value_json.dc_current_1 }}" 91 | - platform: mqtt 92 | name: "Inverter DC voltage 2" 93 | state_topic: "inverter/status" 94 | unit_of_measurement: 'V' 95 | value_template: "{{ value_json.dc_voltage_2 }}" 96 | - platform: mqtt 97 | name: "Inverter DC current 2" 98 | state_topic: "inverter/status" 99 | unit_of_measurement: 'A' 100 | value_template: "{{ value_json.dc_current_2 }}" 101 | - platform: mqtt 102 | name: "Inverter AC current power" 103 | state_topic: "inverter/status" 104 | unit_of_measurement: 'W' 105 | value_template: "{{ value_json.ac_power }}" 106 | - platform: mqtt 107 | name: "Inverter AC current frequency" 108 | state_topic: "inverter/status" 109 | unit_of_measurement: 'Hz' 110 | value_template: "{{ value_json.ac_frequency }}" 111 | - platform: mqtt 112 | name: "Inverter AC voltage 1" 113 | state_topic: "inverter/status" 114 | unit_of_measurement: 'V' 115 | value_template: "{{ value_json.ac_voltage_1 }}" 116 | - platform: mqtt 117 | name: "Inverter AC current 1" 118 | state_topic: "inverter/status" 119 | unit_of_measurement: 'A' 120 | value_template: "{{ value_json.ac_current_1 }}" 121 | - platform: mqtt 122 | name: "Inverter AC voltage 2" 123 | state_topic: "inverter/status" 124 | unit_of_measurement: 'V' 125 | value_template: "{{ value_json.ac_voltage_2 }}" 126 | - platform: mqtt 127 | name: "Inverter AC current 2" 128 | state_topic: "inverter/status" 129 | unit_of_measurement: 'A' 130 | value_template: "{{ value_json.ac_current_2 }}" 131 | - platform: mqtt 132 | name: "Inverter AC voltage 3" 133 | state_topic: "inverter/status" 134 | unit_of_measurement: 'V' 135 | value_template: "{{ value_json.ac_voltage_3 }}" 136 | - platform: mqtt 137 | name: "Inverter AC current 3" 138 | state_topic: "inverter/status" 139 | unit_of_measurement: 'A' 140 | value_template: "{{ value_json.ac_current_3 }}" 141 | - platform: mqtt 142 | name: "Inverter energy total" 143 | state_topic: "inverter/status" 144 | unit_of_measurement: 'kWh' 145 | value_template: "{{ value_json.energy_total }}" 146 | - platform: mqtt 147 | name: "Inverter running time" 148 | state_topic: "inverter/status" 149 | unit_of_measurement: 'h' 150 | value_template: "{{ value_json.running_time }}" 151 | - platform: mqtt 152 | name: "Inverter energy today" 153 | state_topic: "inverter/status" 154 | unit_of_measurement: 'kWh' 155 | value_template: "{{ value_json.energy_today }}" 156 | - platform: mqtt 157 | name: "Inverter module temperature" 158 | state_topic: "inverter/status" 159 | unit_of_measurement: '°C' 160 | value_template: "{{ value_json.temperature_module }}" 161 | - platform: mqtt 162 | name: "Inverter temperature" 163 | state_topic: "inverter/status" 164 | unit_of_measurement: '°C' 165 | value_template: "{{ value_json.temperature_inverter }}" 166 | 167 | # - platform: mqtt 168 | # name: "Inverter bus voltage" 169 | # state_topic: "inverter/status" 170 | # unit_of_measurement: 'V' 171 | # value_template: "{{ value_json.bus_voltage }}" 172 | # - platform: mqtt 173 | # name: "Inverter vice CPU input voltage 1" 174 | # state_topic: "inverter/status" 175 | # unit_of_measurement: 'V' 176 | # value_template: "{{ value_json.vice_cpu_input_voltage_1 }}" 177 | # - platform: mqtt 178 | # name: "Inverter countdown timer" 179 | # state_topic: "inverter/status" 180 | # unit_of_measurement: 'h' 181 | # value_template: "{{ value_json.countdown_timer }}" 182 | # - platform: mqtt 183 | # name: "Inverter PV1 insulation resistance" 184 | # state_topic: "inverter/status" 185 | # unit_of_measurement: 'ohm' 186 | # value_template: "{{ value_json.pv1_insulation_resistance }}" 187 | # - platform: mqtt 188 | # name: "Inverter PV1 insulation resistance" 189 | # state_topic: "inverter/status" 190 | # unit_of_measurement: 'ohm' 191 | # value_template: "{{ value_json.pv2_insulation_resistance }}" 192 | # - platform: mqtt 193 | # name: "Inverter isolation impedance" 194 | # state_topic: "inverter/status" 195 | # value_template: "{{ value_json.isolation_impedance }}" 196 | # - platform: mqtt 197 | # name: "Inverter temperature" 198 | # state_topic: "inverter/status" 199 | # value_template: "{{ value_json.country_code? }}" 200 | # - platform: mqtt 201 | # name: "Inverter phase A distribution" 202 | # state_topic: "inverter/status" 203 | # value_template: "{{ value_json.phase_a_distribution }}" 204 | # - platform: mqtt 205 | # name: "Inverter phase B distribution" 206 | # state_topic: "inverter/status" 207 | # value_template: "{{ value_json.phase_b_distribution }}" 208 | # - platform: mqtt 209 | # name: "Inverter phase C distribution" 210 | # state_topic: "inverter/status" 211 | # value_template: "{{ value_json.phase_c_distribution }}" 212 | ``` 213 | 214 | ## Troubleshoot 215 | 216 | ### No Connection to WIFI 217 | If esp8266 cannot connect to WIFI or you entered wrong password, wait until access point will be exposed, connect to it and enter new WIFI password. You will be prompt for login and password, so enter "admin" as user and password is text which you enter to field "AP password" during first configuration. 218 | 219 | ### RS485 TXD doesn't blink with defined interval (it should blink even without connecting it to invereter ) 220 | 1. Connect to esp8266 with telnet there is a debug console and logs. You need to find IP on your router. 221 | 2. If you see only messages: "(D) Connecting to MQTT server..." it means that MQTT credencials are wrong, connect to configuration web and reconfigure. 222 | 3. If you see messages: "(D) Report status with interval: 1000" (1000 is interval in millies, this example is 1 sec) it means MQTT is configured properly, then 223 | 1. Check if converter is properly power use multimeter 224 | 2. If it's powered, swap A+ and B+ cables maybe they are connected wrongly. If you use Wemos D1 mini, then use D1 and D2 pins for connection with converter. 225 | -------------------------------------------------------------------------------- /custom_firmware/build/esp-07.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawelka/sofarsolar_esp_rs485_logger/636cea39743359228c96b5d392769a979c0ac170/custom_firmware/build/esp-07.bin -------------------------------------------------------------------------------- /custom_firmware/build/wemos_d1_mini.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawelka/sofarsolar_esp_rs485_logger/636cea39743359228c96b5d392769a979c0ac170/custom_firmware/build/wemos_d1_mini.bin -------------------------------------------------------------------------------- /custom_firmware/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [common_env_data] 12 | lib_deps_external = 13 | 256dpi/MQTT 14 | 4-20ma/ModbusMaster 15 | bblanchon/ArduinoJson 16 | prampec/IotWebConf ; V3.x of IotWebConf 17 | joaolopesf/RemoteDebug 18 | 19 | [env:esp07] 20 | platform = espressif8266@2.6.3 21 | board = esp07 22 | framework = arduino 23 | build_flags = -Wl,-Teagle.flash.512k64.ld -DIOTWEBCONF_DEBUG_DISABLED -DHARDWARE_SERIAL 24 | monitor_speed = 9600 25 | monitor_filters = colorize, time 26 | lib_deps = 27 | ${common_env_data.lib_deps_external} 28 | 29 | [env:wemos_d1_mini] 30 | platform = espressif8266@2.6.3 31 | board = d1_mini 32 | framework = arduino 33 | build_flags = 34 | monitor_speed = 9600 35 | monitor_filters = colorize, time 36 | lib_deps = 37 | ${common_env_data.lib_deps_external} 38 | 39 | -------------------------------------------------------------------------------- /custom_firmware/src/Config.cpp: -------------------------------------------------------------------------------- 1 | #include "Config.h" 2 | 3 | 4 | // -- Callback method declarations. 5 | void wifiConnected(); 6 | void configSaved(); 7 | boolean formValidator(iotwebconf::WebRequestWrapper* webRequestWrapper); 8 | 9 | DNSServer dnsServer; 10 | WebServer server(80); 11 | 12 | // Include Update server 13 | #ifdef ESP8266 14 | # include 15 | #elif defined(ESP32) 16 | # include 17 | #endif 18 | 19 | // Create Update Server 20 | #ifdef ESP8266 21 | ESP8266HTTPUpdateServer httpUpdater; 22 | #elif defined(ESP32) 23 | HTTPUpdateServer httpUpdater; 24 | #endif 25 | 26 | char mqttServerValue[STRING_LEN]; 27 | char mqttUserNameValue[STRING_LEN]; 28 | char mqttUserPasswordValue[STRING_LEN]; 29 | char mqttTopicValue[STRING_LEN]; 30 | char checkIntervalValue[NUMBER_LEN]; 31 | 32 | boolean needReset = false; 33 | 34 | IotWebConf iotWebConf(thingName, &dnsServer, &server, wifiInitialApPassword, CONFIG_VERSION); 35 | iotwebconf::ParameterGroup mqttGroup = iotwebconf::ParameterGroup("mqttGroup"); 36 | iotwebconf::TextParameter mqttServerParam = iotwebconf::TextParameter ("MQTT server", "mqttServer", mqttServerValue, STRING_LEN); 37 | iotwebconf::TextParameter mqttUserNameParam = iotwebconf::TextParameter ("MQTT user", "mqttUser", mqttUserNameValue, STRING_LEN); 38 | iotwebconf::PasswordParameter mqttUserPasswordParam = iotwebconf::PasswordParameter("MQTT password", "mqttPass", mqttUserPasswordValue, STRING_LEN, "password"); 39 | iotwebconf::TextParameter mqttTopicParam = iotwebconf::TextParameter ("MQTT topic", "mqttTopic", mqttTopicValue, STRING_LEN, "text", NULL, "inverter/status"); 40 | iotwebconf::NumberParameter checkIntervalParam = iotwebconf::NumberParameter ("Check inverval in sec", "checkInterval", checkIntervalValue, NUMBER_LEN, "300", "1..3600", "min='1' max='3600' step='1'"); 41 | 42 | boolean isNeedReset(){ 43 | return needReset; 44 | } 45 | char* getMqttServerValue(){ 46 | return mqttServerValue; 47 | } 48 | char* getMqttUserNameValue(){ 49 | return mqttUserNameValue; 50 | } 51 | char* getMqttUserPasswordValue(){ 52 | return mqttUserPasswordValue; 53 | } 54 | char* getMqttTopicValue(){ 55 | return mqttTopicValue; 56 | } 57 | int getCheckInterval(){ 58 | return atoi(checkIntervalValue); 59 | } 60 | 61 | void configSetup(){ 62 | iotWebConf.skipApStartup(); 63 | iotWebConf.setStatusPin(STATUS_PIN); 64 | iotWebConf.setConfigPin(CONFIG_PIN); 65 | mqttGroup.addItem(&mqttServerParam); 66 | mqttGroup.addItem(&mqttUserNameParam); 67 | mqttGroup.addItem(&mqttUserPasswordParam); 68 | mqttGroup.addItem(&mqttTopicParam); 69 | mqttGroup.addItem(&checkIntervalParam); 70 | iotWebConf.addParameterGroup(&mqttGroup); 71 | iotWebConf.setConfigSavedCallback(&configSaved); 72 | iotWebConf.setFormValidator(&formValidator); 73 | iotWebConf.setWifiConnectionCallback(&wifiConnected); 74 | iotWebConf.setupUpdateServer( 75 | [](const char* updatePath) { httpUpdater.setup(&server, updatePath); }, 76 | [](const char* userName, char* password) { httpUpdater.updateCredentials(userName, password); }); 77 | 78 | // -- Initializing the configuration. 79 | boolean validConfig = iotWebConf.init(); 80 | if (!validConfig) 81 | { 82 | mqttServerValue[0] = '\0'; 83 | mqttUserNameValue[0] = '\0'; 84 | mqttUserPasswordValue[0] = '\0'; 85 | mqttTopicValue[0] = '\0'; 86 | checkIntervalValue[0] = '\0'; 87 | } 88 | 89 | server.on("/", [] { iotWebConf.handleConfig(); }); 90 | server.onNotFound([]() { iotWebConf.handleNotFound(); }); 91 | 92 | 93 | } 94 | 95 | void configLoop(){ 96 | iotWebConf.doLoop(); 97 | 98 | if (needReset){ 99 | #ifndef DEBUG_DISABLED 100 | Debug.println("Rebooting after 1 second."); 101 | #endif 102 | iotWebConf.delay(1000); 103 | ESP.restart(); 104 | } 105 | 106 | } 107 | 108 | boolean isOnline(){ 109 | return iotWebConf.getState() == iotwebconf::OnLine; 110 | } 111 | 112 | char* getThingName(){ 113 | return iotWebConf.getThingName(); 114 | } 115 | 116 | void configSaved() 117 | { 118 | #ifndef DEBUG_DISABLED 119 | Debug.println("Configuration was updated."); 120 | #endif 121 | needReset = true; 122 | } 123 | 124 | boolean formValidator(iotwebconf::WebRequestWrapper* webRequestWrapper) 125 | { 126 | #ifndef DEBUG_DISABLED 127 | Debug.println("Validating form."); 128 | #endif 129 | boolean valid = true; 130 | 131 | int l = server.arg(mqttServerParam.getId()).length(); 132 | if (l < 3) 133 | { 134 | mqttServerParam.errorMessage = "Please provide at least 3 characters!"; 135 | valid = false; 136 | } 137 | 138 | return valid; 139 | } 140 | -------------------------------------------------------------------------------- /custom_firmware/src/Config.h: -------------------------------------------------------------------------------- 1 | #ifndef Config_h 2 | #define Config_h 3 | 4 | 5 | 6 | #include "Arduino.h" 7 | #include "Debug.h" 8 | #include 9 | #include 10 | 11 | const char thingName[] = "sofarsolar2mqtt"; 12 | const char wifiInitialApPassword[] = "sofarsolar2mqtt"; 13 | 14 | #define CONFIG_VERSION "0001" 15 | //#define CONFIG_PIN D2 //wemos 16 | #define CONFIG_PIN 4 //esp07 17 | #define STATUS_PIN LED_BUILTIN 18 | 19 | #define STRING_LEN 128 20 | #define NUMBER_LEN 32 21 | 22 | 23 | char* getThingName(); 24 | boolean isNeedReset(); 25 | char* getMqttServerValue(); 26 | char* getMqttUserNameValue(); 27 | char* getMqttUserPasswordValue(); 28 | char* getMqttTopicValue(); 29 | int getCheckInterval(); 30 | 31 | 32 | void configSetup(); 33 | void configLoop(); 34 | boolean isOnline(); 35 | 36 | 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /custom_firmware/src/Debug.h: -------------------------------------------------------------------------------- 1 | #ifndef Debug_h 2 | #define Debug_h 3 | 4 | #include "Arduino.h" 5 | #include "RemoteDebug.h" 6 | 7 | extern RemoteDebug Debug; 8 | 9 | #endif -------------------------------------------------------------------------------- /custom_firmware/src/InverterValue.cpp: -------------------------------------------------------------------------------- 1 | #include "InverterValue.h" 2 | 3 | InverterValue::InverterValue(String valueName, int byteno, double divider, String unit) : InverterValue(valueName, byteno, 1, divider, unit){ 4 | } 5 | 6 | InverterValue::InverterValue(String valueName, int byteno, int size, double divider, String unit){ 7 | this->valueName = valueName; 8 | this->divider = divider; 9 | this->unit = unit; 10 | this->byteno = byteno; 11 | this->size = size; 12 | } 13 | String InverterValue::getValueName(){ 14 | return this->valueName; 15 | } 16 | 17 | double InverterValue::getValue(ModbusMaster* master){ 18 | if(size == 1){ 19 | return (double) master->getResponseBuffer(this->byteno) / this->divider; 20 | }else{ //size 2 21 | return ((double) master->getResponseBuffer(this->byteno) * 256.0 + (double) master->getResponseBuffer(this->byteno + 1)) / this->divider; 22 | } 23 | } 24 | 25 | String BYTE0_FAULTS[] = {"GridOVP", "GridUVP", "GridOFP", "GridUFP", "BatOVP"}; 26 | 27 | String InverterValue::getStrValue(ModbusMaster* master){ 28 | if(this->byteno == -1){ 29 | if(master->getResponseBuffer(0) == 0){ 30 | if (master->getResponseBuffer(14) == 0 && master->getResponseBuffer(16) == 0 && master->getResponseBuffer(18) == 0){ //ac voltage zero 31 | return "off"; 32 | }else if (master->getResponseBuffer(11) == 0){ //ac power zero 33 | return "standby"; 34 | }else{ 35 | return "normal"; 36 | } 37 | }else{ 38 | return "fault"; 39 | } 40 | }else if(this->byteno == 0){ 41 | int value = master->getResponseBuffer(0); 42 | String result = ""; 43 | for(int i = 0; i < 5; i++){ 44 | if( ((value >> i) & 0x1) == 1 ){ 45 | if(result.length() > 0){ 46 | result +=","; 47 | } 48 | result += BYTE0_FAULTS[i]; 49 | } 50 | } 51 | return result; 52 | }else{ 53 | return ""; 54 | } 55 | } 56 | 57 | String InverterValue::getUnit(){ 58 | return this->unit; 59 | }; -------------------------------------------------------------------------------- /custom_firmware/src/InverterValue.h: -------------------------------------------------------------------------------- 1 | #ifndef InverterValue_h 2 | #define InverterValue_h 3 | 4 | #include 5 | #include 6 | 7 | class InverterValue { 8 | 9 | private: 10 | String valueName; 11 | double divider; 12 | String unit; 13 | int byteno; 14 | int size; 15 | public: 16 | InverterValue(String valueName, int byteno, double divider, String unit); 17 | InverterValue(String valueName, int byteno, int size, double divider, String unit); 18 | String getValueName(); 19 | double getValue(ModbusMaster* master); 20 | String getStrValue(ModbusMaster* master); 21 | String getUnit(); 22 | 23 | }; 24 | 25 | #endif -------------------------------------------------------------------------------- /custom_firmware/src/main.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Sofar Solar modbus2mqtt integration 3 | * 4 | * Copyright (C) 2018 Balazs Kelemen 5 | * 6 | * This software may be modified and distributed under the terms 7 | * of the MIT license. See the LICENSE file for details. 8 | */ 9 | #include 10 | 11 | //#define HARDWARE_SERIAL 12 | 13 | #include "Config.h" 14 | #include "Debug.h" 15 | #include "InverterValue.h" 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | WiFiClient net; 22 | MQTTClient mqttClient(1500); 23 | boolean skipUnknown = true; 24 | 25 | boolean needMqttConnect = false; 26 | unsigned long lastReport = 0; 27 | unsigned long lastMqttConnectionAttempt = 0; 28 | 29 | #ifndef HARDWARE_SERIAL 30 | #include // Modbus RTU pins D7(13),D8(15) RX,TX 31 | SoftwareSerial serial2(D1, D2); //RX, TX 32 | #endif 33 | ModbusMaster node; 34 | 35 | boolean connectMqtt(); 36 | 37 | const int INVERTER_BUFFER_TO_READ = 47; 38 | const int INVERTER_VALUES_SIZE = 46; 39 | const InverterValue INVERTER_VALUES[] = { 40 | InverterValue("status", -1, 1, ""), //pseudo value calcualted from fault message 41 | InverterValue("fault_message", 0, 1, ""), 42 | InverterValue("unknown_2", 1, 1, "?"), 43 | InverterValue("unknown_3", 2, 1, "?"), 44 | InverterValue("unknown_4", 3, 1, "?"), 45 | InverterValue("unknown_5", 4, 1, "?"), 46 | InverterValue("dc_voltage_1", 5, 10, "V"), 47 | InverterValue("dc_current_1", 6, 100, "A"), 48 | InverterValue("dc_voltage_2", 7, 10, "V"), 49 | InverterValue("dc_current_2", 8, 100, "A"), 50 | InverterValue("unknown_10", 9, 1, "?"), 51 | InverterValue("unknown_11", 10, 1, "?"), 52 | InverterValue("ac_power", 11, 0.1, "W"), //ac power 53 | InverterValue("unknown_13", 12, 1, "?"), 54 | InverterValue("ac_frequency", 13, 100, "Hz"), 55 | InverterValue("ac_voltage_1", 14, 10, "V"), 56 | InverterValue("ac_current_1", 15, 100, "A"), 57 | InverterValue("ac_voltage_2", 16, 10, "V"), 58 | InverterValue("ac_current_2", 17, 100, "A"), 59 | InverterValue("ac_voltage_3", 18, 10, "V"), 60 | InverterValue("ac_current_3", 19, 100, "A"), 61 | InverterValue("energy_total", 20, 2, 1, "kWh"), // total energy 62 | InverterValue("running_time", 22, 2, 1, "h"), //total operational time 63 | InverterValue("energy_today", 24, 100, "kWh"), 64 | InverterValue("unknown_26", 25, 1, "?"), //static value? during fault (no grid) stays the same 65 | InverterValue("temperature_module", 26, 1, "C"), //module temperature 66 | InverterValue("temperature_inverter", 27, 1, "C"), //inverter temperature 67 | InverterValue("bus_voltage", 28, 10, "V"), //bus voltage 68 | InverterValue("vice_cpu_input_voltage_1", 29, 10, "V"), // Vice CPU input voltage 1 69 | InverterValue("unknown_31", 30, 1, "?"), //during fault (no grid) there is an value 70 | InverterValue("countdown_timer", 31, 1, "h"), //count down timer (h) 71 | InverterValue("unknown_33", 32, 1, "?"), 72 | InverterValue("unknown_34", 33, 1, "?"), 73 | InverterValue("unknown_35", 34, 1, "?"), 74 | InverterValue("pv1_insulation_resistance", 35, 1, "Ohm"), //PV1 insulation resistance 75 | InverterValue("pv2_insulation_resistance", 36, 1, "Ohm"), //PV2 insulation resistance 76 | InverterValue("isolation_impedance", 37, 1, ""), //isolation impedance - cathone to ground 77 | InverterValue("country_code?", 38, 1, ""), //country code 78 | InverterValue("unknown_40", 39, 1, "?"), 79 | InverterValue("unknown_41", 40, 1, "?"), 80 | InverterValue("unknown_42", 41, 1, "?"), //status ???? 81 | InverterValue("phase_a_distribution", 42, 1, ""), //phase A distribution 82 | InverterValue("phase_b_distribution", 43, 1, ""), //phase B distribution 83 | InverterValue("phase_c_distribution", 44, 1, ""), //phase C distribution 84 | InverterValue("unknown_46", 45, 1, "?"), 85 | InverterValue("unknown_47", 46, 1, "?") 86 | }; 87 | 88 | RemoteDebug Debug; 89 | int reportInterval; 90 | 91 | void setup() 92 | { 93 | Serial.begin(9600); 94 | #ifndef DEBUG_DISABLED 95 | Debug.println("Starting up..."); 96 | #endif 97 | 98 | configSetup(); 99 | 100 | mqttClient.begin(getMqttServerValue(), net); 101 | 102 | #ifdef HARDWARE_SERIAL 103 | node.begin(1, Serial); 104 | #else 105 | serial2.begin(9600); 106 | node.begin(1, serial2); 107 | #endif 108 | delay(100); 109 | 110 | reportInterval = getCheckInterval() * 1000; 111 | 112 | Debug.begin("sofarsolar2mqtt"); // Initialize the WiFi server 113 | Debug.setResetCmdEnabled(true); // Enable the reset command 114 | Debug.showColors(false); 115 | 116 | 117 | #ifndef DEBUG_DISABLED 118 | Debug.println("Ready."); 119 | Debug.print("Report interval: "); 120 | Debug.println(reportInterval); 121 | #endif 122 | } 123 | 124 | char message[1500]; 125 | DynamicJsonDocument doc(1500); 126 | int msgCount = 0; 127 | 128 | void modbusLoop() 129 | { 130 | unsigned long startTime = millis(); 131 | if ((reportInterval < startTime - lastReport) && mqttClient.connected()) 132 | { 133 | Debug.print("Report status with interval: "); 134 | Debug.println(reportInterval); 135 | 136 | 137 | doc.clear(); 138 | node.clearResponseBuffer(); 139 | int status = node.readHoldingRegisters(1, INVERTER_BUFFER_TO_READ); 140 | 141 | if (status == ModbusMaster::ku8MBSuccess){ 142 | for(int idx = 0; idx < INVERTER_VALUES_SIZE; idx++){ 143 | InverterValue curr = INVERTER_VALUES[idx]; 144 | if(skipUnknown && curr.getUnit()[0] == '?'){ 145 | continue; 146 | } 147 | if( idx <= 1){ 148 | doc[curr.getValueName()] = curr.getStrValue(&node); 149 | }else{ 150 | doc[curr.getValueName()] = curr.getValue(&node); 151 | } 152 | } 153 | 154 | msgCount++; 155 | #ifndef DEBUG_DISABLED 156 | Debug.printf("Sending on MQTT channel '%s': %d \n", getMqttTopicValue(), msgCount); 157 | #endif 158 | 159 | serializeJsonPretty(doc, message); 160 | mqttClient.publish(getMqttTopicValue(), message, true, 1); 161 | } 162 | 163 | lastReport = startTime; 164 | 165 | #ifndef DEBUG_DISABLED 166 | Debug.printf("Loop time: %d \n", (millis() - startTime)); 167 | #endif 168 | } 169 | } 170 | 171 | void loop() 172 | { 173 | Debug.handle(); 174 | // debugHandle(); // Equal to SerialDebug 175 | 176 | configLoop(); 177 | mqttClient.loop(); 178 | 179 | if (needMqttConnect) 180 | { 181 | if (connectMqtt()) 182 | { 183 | needMqttConnect = false; 184 | } 185 | } 186 | else if ( isOnline() && !mqttClient.connected()) 187 | { 188 | #ifndef DEBUG_DISABLED 189 | Debug.println("MQTT reconnect"); 190 | #endif 191 | connectMqtt(); 192 | } 193 | 194 | modbusLoop(); 195 | } 196 | 197 | void wifiConnected() 198 | { 199 | needMqttConnect = true; 200 | } 201 | 202 | boolean connectMqttOptions() 203 | { 204 | boolean result; 205 | if (getMqttUserPasswordValue()[0] != '\0') 206 | { 207 | result = mqttClient.connect(getThingName(), getMqttUserNameValue(), getMqttUserPasswordValue()); 208 | } 209 | else if (getMqttUserNameValue()[0] != '\0') 210 | { 211 | result = mqttClient.connect(getThingName(), getMqttUserNameValue()); 212 | } 213 | else 214 | { 215 | result = mqttClient.connect(getThingName()); 216 | } 217 | return result; 218 | } 219 | 220 | boolean connectMqtt() 221 | { 222 | unsigned long now = millis(); 223 | if (15000 > now - lastMqttConnectionAttempt) 224 | { 225 | // Do not repeat within 1 sec. 226 | return false; 227 | } 228 | #ifndef DEBUG_DISABLED 229 | Debug.println("Connecting to MQTT server..."); 230 | #endif 231 | if (!connectMqttOptions()) 232 | { 233 | lastMqttConnectionAttempt = now; 234 | return false; 235 | } 236 | #ifndef DEBUG_DISABLED 237 | Debug.println("Connected!"); 238 | #endif 239 | 240 | return true; 241 | } -------------------------------------------------------------------------------- /esphome_config/README.md: -------------------------------------------------------------------------------- 1 | # ESPHome modbus 2 | 3 | Here as a full config used by me to integrated with inverter. After that you see device in HA. 4 | 5 | Config utilize ESPHome modbus feature https://esphome.io/components/modbus.html 6 | 7 | Any suggestion how to improve config are welcome. Potencial improvements 8 | * [DONE] better support for fault code. Right now it's a number only 9 | * [TODO?] all registers or only useful one? 10 | 11 | ## Config 12 | 13 | ``` 14 | substitutions: 15 | devicename: sofarsolar-logger 16 | friendly_name: SofarSolar 17 | 18 | wifi: 19 | ssid: !secret wifi-ssid 20 | password: !secret wifi-password 21 | fast_connect: true 22 | 23 | # Enable fallback hotspot (captive portal) in case wifi connection fails 24 | ap: 25 | ssid: "sweet-fallback-${devicename}" 26 | password: !secret wifi-fallback-password 27 | 28 | captive_portal: 29 | 30 | # Enable Home Assistant API 31 | api: 32 | password: !secret api-password 33 | 34 | ota: 35 | password: !secret ota-password 36 | 37 | # Enable Web server 38 | web_server: 39 | port: 80 40 | 41 | esphome: 42 | name: $devicename 43 | platform: ESP8266 44 | board: esp01_1m 45 | board_flash_mode: dout 46 | 47 | logger: 48 | level: INFO 49 | baud_rate: 0 50 | 51 | uart: 52 | id: mod_bus 53 | tx_pin: 1 54 | rx_pin: 3 55 | baud_rate: 9600 56 | stop_bits: 1 57 | 58 | modbus: 59 | id: mod_bus_sofar 60 | 61 | modbus_controller: 62 | - id: sofarsolar 63 | address: 0x01 64 | modbus_id: mod_bus_sofar 65 | update_interval: 10s 66 | 67 | text_sensor: 68 | - platform: modbus_controller 69 | modbus_controller_id: sofarsolar 70 | name: ${friendly_name} Status 71 | id: inverter_status 72 | register_type: holding 73 | address: 0x0000 74 | response_size: 2 75 | lambda: |- 76 | auto z = "Unknown"; 77 | char d = data[item->offset+1]; 78 | if (d == 0) z = "Wait"; 79 | else if (d == 1) z = "Check"; 80 | else if (d == 2) z = "Normal"; 81 | else if (d == 3) z = "Fault"; 82 | else if (d == 4) z = "Permanent"; 83 | return {z}; 84 | - platform: modbus_controller 85 | modbus_controller_id: sofarsolar 86 | name: ${friendly_name} Fault Message 87 | id: inverter_fault_message 88 | register_type: holding 89 | address: 0x0001 90 | response_size: 10 91 | lambda: |- 92 | std::string z = ""; 93 | int idx = item->offset; 94 | //byte[0] 95 | if ((data[idx] & 0x1) != 0) z += "GridOVP,"; 96 | if ((data[idx] & 0x2) != 0) z += "GridUVP,"; 97 | if ((data[idx] & 0x4) != 0) z += "GridOFP,"; 98 | if ((data[idx] & 0x8) != 0) z += "GridUFP,"; 99 | if ((data[idx] & 0x10) != 0) z += "PVUVP,"; 100 | if ((data[idx] & 0x20) != 0) z += "GridLVRT,"; 101 | if ((data[idx] & 0x40) != 0) z += "reserve-ID7,"; 102 | if ((data[idx] & 0x80) != 0) z += "reserve-ID8,"; 103 | //byte[1] 104 | idx++; 105 | if ((data[idx] & 0x1) != 0) z += "PVOVP,"; 106 | if ((data[idx] & 0x2) != 0) z += "IpvUnbalance,"; 107 | if ((data[idx] & 0x4) != 0) z += "PvConfigSetWrong,"; 108 | if ((data[idx] & 0x8) != 0) z += "GFCIFault,"; 109 | if ((data[idx] & 0x10) != 0) z += "PhaseSequenceFault,"; 110 | if ((data[idx] & 0x20) != 0) z += "HwBoostOCP,"; 111 | if ((data[idx] & 0x40) != 0) z += "HwAcOCP,"; 112 | if ((data[idx] & 0x80) != 0) z += "AcRmsOCP,"; 113 | //byte[2] 114 | idx++; 115 | if ((data[idx] & 0x1) != 0) z += "HwADFaultIGrid,"; 116 | if ((data[idx] & 0x2) != 0) z += "HwADFaultDCI,"; 117 | if ((data[idx] & 0x4) != 0) z += "HwADFaultVGrid,"; 118 | if ((data[idx] & 0x8) != 0) z += "GFCIDeviceFault,"; 119 | if ((data[idx] & 0x10) != 0) z += "MChip_Fault,"; 120 | if ((data[idx] & 0x20) != 0) z += "HwAuxPowerFault,"; 121 | if ((data[idx] & 0x40) != 0) z += "BusVoltZeroFault,"; 122 | if ((data[idx] & 0x80) != 0) z += "IacRmsUnbalance,"; 123 | //byte[3] 124 | idx++; 125 | if ((data[idx] & 0x1) != 0) z += "BusUVP,"; 126 | if ((data[idx] & 0x2) != 0) z += "BusOVP,"; 127 | if ((data[idx] & 0x4) != 0) z += "VbusUnbalance,"; 128 | if ((data[idx] & 0x8) != 0) z += "DciOCP,"; 129 | if ((data[idx] & 0x10) != 0) z += "SwOCPInstant,"; 130 | if ((data[idx] & 0x20) != 0) z += "SwBOCPInstant,"; 131 | if ((data[idx] & 0x40) != 0) z += "reserved-ID31,"; 132 | if ((data[idx] & 0x80) != 0) z += "reserved-ID32,"; 133 | //byte[4] 134 | idx++; 135 | if (data[idx] != 0) z += "reserved-ID33~40,"; 136 | //byte[5] 137 | idx++; 138 | if (data[idx] != 0) z += "reserved-ID41~48,"; 139 | //byte[6] 140 | idx++; 141 | if ((data[idx] & 0x1) != 0) z += "ConsistentFault_VGrid,"; 142 | if ((data[idx] & 0x2) != 0) z += "ConsistentFault_FGrid,"; 143 | if ((data[idx] & 0x4) != 0) z += "ConsistentFault_DCI,"; 144 | if ((data[idx] & 0x8) != 0) z += "ConsistentFault_GFCI,"; 145 | if ((data[idx] & 0x10) != 0) z += "SpiCommLose,"; 146 | if ((data[idx] & 0x20) != 0) z += "SciCommLose,"; 147 | if ((data[idx] & 0x40) != 0) z += "RelayTestFail,"; 148 | if ((data[idx] & 0x80) != 0) z += "PvIsoFault,"; 149 | //byte[7] 150 | idx++; 151 | if ((data[idx] & 0x1) != 0) z += "OverTempFault_Inv,"; 152 | if ((data[idx] & 0x2) != 0) z += "OverTempFault_Boost,"; 153 | if ((data[idx] & 0x4) != 0) z += "OverTempFault_Env,"; 154 | if ((data[idx] & 0x8) != 0) z += "PEConnectFault,"; 155 | if ((data[idx] & 0x10) != 0) z += "reserved-ID61,"; 156 | if ((data[idx] & 0x20) != 0) z += "reserved-ID62,"; 157 | if ((data[idx] & 0x40) != 0) z += "reserved-ID63,"; 158 | if ((data[idx] & 0x80) != 0) z += "reserved-ID64,"; 159 | //byte[8] 160 | idx++; 161 | if ((data[idx] & 0x1) != 0) z += "unrecoverHwAcOCP,"; 162 | if ((data[idx] & 0x2) != 0) z += "unrecoverBusOVP,"; 163 | if ((data[idx] & 0x4) != 0) z += "unrecoverIacRmsUnbalance,"; 164 | if ((data[idx] & 0x8) != 0) z += "unrecoverIpvUnbalance,"; 165 | if ((data[idx] & 0x10) != 0) z += "unrecoverVbusUnbalance,"; 166 | if ((data[idx] & 0x20) != 0) z += "unrecoverOCPInstant,"; 167 | if ((data[idx] & 0x40) != 0) z += "unrecoverPvConfigSetWrong,"; 168 | if ((data[idx] & 0x80) != 0) z += "reserved-ID72,"; 169 | //byte[9] 170 | idx++; 171 | if ((data[idx] & 0x1) != 0) z += "reserved-ID73,"; 172 | if ((data[idx] & 0x2) != 0) z += "unrecoverIPVInstant,"; 173 | if ((data[idx] & 0x4) != 0) z += "unrecoverWRITEEEPROM,"; 174 | if ((data[idx] & 0x8) != 0) z += "unrecoverREADEEPROM,"; 175 | if ((data[idx] & 0x10) != 0) z += "unrecoverRelayFail,"; 176 | if ((data[idx] & 0x20) != 0) z += "reserved-ID78,"; 177 | if ((data[idx] & 0x40) != 0) z += "reserved-ID79,"; 178 | if ((data[idx] & 0x80) != 0) z += "reserved-ID80,"; 179 | if(z.length() > 0){ 180 | z.pop_back(); 181 | } 182 | return {z}; 183 | sensor: 184 | - platform: wifi_signal 185 | id: inverter_wifi_signal 186 | name: ${friendly_name} WiFi Signal 187 | update_interval: 60s 188 | - platform: uptime 189 | id: inverter_uptime 190 | name: ${friendly_name} Uptime 191 | filters: 192 | - lambda: return x / 60.0; 193 | unit_of_measurement: minutes 194 | - platform: modbus_controller 195 | modbus_controller_id: sofarsolar 196 | name: ${friendly_name} DC1 Voltage 197 | id: inverter_dc_v1 198 | register_type: holding 199 | address: 0x0006 200 | unit_of_measurement: "V" 201 | icon: "mdi:alpha-v-circle-outline" 202 | value_type: U_WORD 203 | accuracy_decimals: 1 204 | filters: 205 | - multiply: 0.1 206 | - platform: modbus_controller 207 | modbus_controller_id: sofarsolar 208 | name: ${friendly_name} DC1 Current 209 | id: inverter_dc_c1 210 | register_type: holding 211 | address: 0x0007 212 | unit_of_measurement: "A" 213 | icon: "mdi:alpha-a-circle-outline" 214 | value_type: U_WORD 215 | accuracy_decimals: 2 216 | filters: 217 | - multiply: 0.01 218 | - platform: modbus_controller 219 | modbus_controller_id: sofarsolar 220 | name: ${friendly_name} DC2 Voltage 221 | id: inverter_dc_v2 222 | register_type: holding 223 | address: 0x0008 224 | unit_of_measurement: "V" 225 | icon: "mdi:alpha-v-circle-outline" 226 | value_type: U_WORD 227 | accuracy_decimals: 1 228 | filters: 229 | - multiply: 0.1 230 | - platform: modbus_controller 231 | modbus_controller_id: sofarsolar 232 | name: ${friendly_name} DC2 Current 233 | id: inverter_dc_c2 234 | register_type: holding 235 | address: 0x0009 236 | unit_of_measurement: "A" 237 | icon: "mdi:alpha-a-circle-outline" 238 | value_type: U_WORD 239 | accuracy_decimals: 2 240 | filters: 241 | - multiply: 0.01 242 | - platform: modbus_controller 243 | modbus_controller_id: sofarsolar 244 | name: ${friendly_name} DC1 Power 245 | id: inverter_dc_power1 246 | register_type: holding 247 | address: 0x000a 248 | unit_of_measurement: "W" 249 | device_class: "power" 250 | value_type: U_WORD 251 | filters: 252 | - multiply: 10 253 | - platform: modbus_controller 254 | modbus_controller_id: sofarsolar 255 | name: ${friendly_name} DC2 Power 256 | id: inverter_dc_power2 257 | register_type: holding 258 | address: 0x000b 259 | unit_of_measurement: "W" 260 | device_class: "power" 261 | value_type: U_WORD 262 | filters: 263 | - multiply: 10 264 | - platform: modbus_controller 265 | modbus_controller_id: sofarsolar 266 | name: ${friendly_name} AC Power 267 | id: inverter_ac_power 268 | register_type: holding 269 | address: 0x000c 270 | unit_of_measurement: "W" 271 | device_class: "power" 272 | value_type: U_WORD 273 | filters: 274 | - multiply: 10 275 | - platform: modbus_controller 276 | modbus_controller_id: sofarsolar 277 | name: ${friendly_name} AC Reactive Power 278 | id: inverter_ac_reactive_power 279 | register_type: holding 280 | address: 0x000d 281 | unit_of_measurement: "Var" 282 | device_class: "power" 283 | value_type: S_WORD 284 | filters: 285 | - multiply: 10 286 | - platform: modbus_controller 287 | modbus_controller_id: sofarsolar 288 | name: ${friendly_name} AC Freq 289 | id: inverter_ac_freq 290 | register_type: holding 291 | address: 0x000e 292 | unit_of_measurement: "Hz" 293 | icon: "mdi:current-ac" 294 | value_type: U_WORD 295 | accuracy_decimals: 2 296 | filters: 297 | - multiply: 0.01 298 | - platform: modbus_controller 299 | modbus_controller_id: sofarsolar 300 | name: ${friendly_name} AC1 Voltage 301 | id: inverter_ac_v1 302 | register_type: holding 303 | address: 0x000f 304 | unit_of_measurement: "V" 305 | icon: "mdi:alpha-v-circle-outline" 306 | value_type: U_WORD 307 | accuracy_decimals: 1 308 | filters: 309 | - multiply: 0.1 310 | - platform: modbus_controller 311 | modbus_controller_id: sofarsolar 312 | name: ${friendly_name} AC1 Current 313 | id: inverter_ac_c1 314 | register_type: holding 315 | address: 0x0010 316 | unit_of_measurement: "A" 317 | icon: "mdi:alpha-a-circle-outline" 318 | value_type: U_WORD 319 | accuracy_decimals: 2 320 | filters: 321 | - multiply: 0.01 322 | - platform: modbus_controller 323 | modbus_controller_id: sofarsolar 324 | name: ${friendly_name} AC2 Voltage 325 | id: inverter_ac_v2 326 | register_type: holding 327 | address: 0x0011 328 | unit_of_measurement: "V" 329 | icon: "mdi:alpha-v-circle-outline" 330 | value_type: U_WORD 331 | accuracy_decimals: 1 332 | filters: 333 | - multiply: 0.1 334 | - platform: modbus_controller 335 | modbus_controller_id: sofarsolar 336 | name: ${friendly_name} AC2 Current 337 | id: inverter_ac_c2 338 | register_type: holding 339 | address: 0x0012 340 | unit_of_measurement: "A" 341 | icon: "mdi:alpha-a-circle-outline" 342 | value_type: U_WORD 343 | accuracy_decimals: 2 344 | filters: 345 | - multiply: 0.01 346 | - platform: modbus_controller 347 | modbus_controller_id: sofarsolar 348 | name: ${friendly_name} AC3 Voltage 349 | id: inverter_ac_v3 350 | register_type: holding 351 | address: 0x0013 352 | unit_of_measurement: "V" 353 | icon: "mdi:alpha-v-circle-outline" 354 | value_type: U_WORD 355 | accuracy_decimals: 1 356 | filters: 357 | - multiply: 0.1 358 | - platform: modbus_controller 359 | modbus_controller_id: sofarsolar 360 | name: ${friendly_name} AC3 Current 361 | id: inverter_ac_c3 362 | register_type: holding 363 | address: 0x0014 364 | unit_of_measurement: "A" 365 | icon: "mdi:alpha-a-circle-outline" 366 | value_type: U_WORD 367 | accuracy_decimals: 2 368 | filters: 369 | - multiply: 0.01 370 | - platform: modbus_controller 371 | modbus_controller_id: sofarsolar 372 | name: ${friendly_name} Energy total 373 | id: inverter_energy_total 374 | register_type: holding 375 | address: 0x0015 376 | unit_of_measurement: "kWh" 377 | device_class: "energy" 378 | value_type: U_DWORD 379 | - platform: modbus_controller 380 | modbus_controller_id: sofarsolar 381 | name: ${friendly_name} Energy generation time total 382 | id: inverter_energy_generation_time_total 383 | register_type: holding 384 | address: 0x0017 385 | unit_of_measurement: "h" 386 | value_type: U_DWORD 387 | - platform: modbus_controller 388 | modbus_controller_id: sofarsolar 389 | name: ${friendly_name} Energy today 390 | id: inverter_energy_today 391 | register_type: holding 392 | address: 0x0019 393 | unit_of_measurement: "kWh" 394 | device_class: "energy" 395 | value_type: U_WORD 396 | accuracy_decimals: 2 397 | filters: 398 | - multiply: 0.01 399 | - platform: modbus_controller 400 | modbus_controller_id: sofarsolar 401 | name: ${friendly_name} Energy generation time today 402 | id: inverter_energy_generation_time_today 403 | register_type: holding 404 | address: 0x001A 405 | unit_of_measurement: "min" 406 | value_type: U_WORD 407 | - platform: modbus_controller 408 | modbus_controller_id: sofarsolar 409 | name: ${friendly_name} Temprature module 410 | id: inverter_temp_module 411 | register_type: holding 412 | address: 0x001B 413 | unit_of_measurement: "°C" 414 | device_class: "temperature" 415 | value_type: U_WORD 416 | - platform: modbus_controller 417 | modbus_controller_id: sofarsolar 418 | name: ${friendly_name} Temprature inverter 419 | id: inverter_temp_inverter 420 | register_type: holding 421 | address: 0x001C 422 | unit_of_measurement: "°C" 423 | device_class: "temperature" 424 | value_type: U_WORD 425 | - platform: modbus_controller 426 | modbus_controller_id: sofarsolar 427 | name: ${friendly_name} Bus voltage 428 | id: inverter_bus_voltage 429 | register_type: holding 430 | address: 0x001D 431 | unit_of_measurement: "V" 432 | icon: "mdi:alpha-v-circle-outline" 433 | value_type: U_WORD 434 | accuracy_decimals: 1 435 | filters: 436 | - multiply: 0.1 437 | 438 | # Basic switch to allow you to restart the device remotely 439 | switch: 440 | - platform: restart 441 | name: ${friendly_name} Restart 442 | 443 | binary_sensor: 444 | - platform: status 445 | name: ${friendly_name} Status 446 | ``` 447 | 448 | ## Image 449 | ![](../images/esphome_ha.png) 450 | -------------------------------------------------------------------------------- /images/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawelka/sofarsolar_esp_rs485_logger/636cea39743359228c96b5d392769a979c0ac170/images/config.png -------------------------------------------------------------------------------- /images/desk1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawelka/sofarsolar_esp_rs485_logger/636cea39743359228c96b5d392769a979c0ac170/images/desk1.jpg -------------------------------------------------------------------------------- /images/desk2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawelka/sofarsolar_esp_rs485_logger/636cea39743359228c96b5d392769a979c0ac170/images/desk2.jpg -------------------------------------------------------------------------------- /images/esphome_ha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawelka/sofarsolar_esp_rs485_logger/636cea39743359228c96b5d392769a979c0ac170/images/esphome_ha.png -------------------------------------------------------------------------------- /images/inverter1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawelka/sofarsolar_esp_rs485_logger/636cea39743359228c96b5d392769a979c0ac170/images/inverter1.jpg -------------------------------------------------------------------------------- /images/inverter2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawelka/sofarsolar_esp_rs485_logger/636cea39743359228c96b5d392769a979c0ac170/images/inverter2.jpg -------------------------------------------------------------------------------- /images/prototype.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawelka/sofarsolar_esp_rs485_logger/636cea39743359228c96b5d392769a979c0ac170/images/prototype.jpg -------------------------------------------------------------------------------- /images/schematic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawelka/sofarsolar_esp_rs485_logger/636cea39743359228c96b5d392769a979c0ac170/images/schematic.jpg -------------------------------------------------------------------------------- /images/tasmota.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawelka/sofarsolar_esp_rs485_logger/636cea39743359228c96b5d392769a979c0ac170/images/tasmota.png -------------------------------------------------------------------------------- /tasmota_smi/README.md: -------------------------------------------------------------------------------- 1 | # Tasmota SMI 2 | 3 | The script below utilize Tasmota SMI feature https://tasmota.github.io/docs/Smart-Meter-Interface/ 4 | 5 | In user_config_override.h you will find config for custom built to minimalize firmware size and be able to update it over OTA without switching to minimal image. 6 | 7 | You need to set module Generic module (18) last on the list and afer that enable and use script below. 8 | 9 | Script utilize pins 3 for RX and 1 for TX. It's first "3" and last "1" in line "+1,3,m,1,9600,Sofar,1,100,r010300010009,r0103000c0009,r01030015000c" 10 | 11 | ``` 12 | >D 13 | >B 14 | ->sensor53 r 15 | ->sensor53 d0 16 | >M 1 17 | +1,3,m,1,9600,Sofar,1,100,r010300010009,r0103000c0009,r01030015000c 18 | 19 | 1,010312UUuux8x8@i0:1,Fault code,,fault,0 20 | 1,010312x8x2UUuux6@i0:10,DC V1,V,dc_v1 DC1,1 21 | 1,010312x8x4UUuux4@i0:100,DC C1,A,dc_c1,2 22 | 1,010312x8x6UUuux2@i0:10,DC V2,V,dc_v2,1 23 | 1,010312x8x8UUuu@i0:100,DC C2,A,dc_c2,2 24 | 25 | 1,010312UUuux8x8@i1:0.1,AC power,W,ac_power,0 26 | 1,010312x4UUuux6x6@i1:100,AC freq,Hz,ac_freq,2 27 | 1,010312x6UUuux6x4@i1:10,AC V1,V,ac_v1,1 28 | 1,010312x8UUuux6x2@i1:100,AC C1,A,ac_c1,2 29 | 1,010312x8x2UUuux6@i1:10,AC V2,V,ac_v2,1 30 | 1,010312x8x4x4UUuux4@i1:100,AC C2,A,ac_c2,2 31 | 1,010312x8x6UUuux2@i1:10,AC V3,V,ac_v3,1 32 | 1,010312x8x8UUuu@i1:100,AC C3,A,ac_c3,2 33 | 34 | 1,010318UUuuUUuux8x8x6@i2:1,Energy total,kWh,energy_total,0 35 | 1,010318x4UUuuUUuux8x8x2@i2:1,Running time,h,running_time,0 36 | 1,010318x8UUuux8x8@i2:100,Energy today,kWh,energy_today,2 37 | 1,010318x8x4UUuux8x4@i2:1,Temp module,°C,temp_module,0 38 | 1,010318x8x6UUuux8x2@i2:1,Temp inverter,°C,temp_inverter,0 39 | 1,010318x8x8UUuux8@i2:10,Bus voltage,V,bus_voltage,1 40 | 1,010318x8x8x6UUuux2@i2:1,Countdown timer,h,countdown_timer,0 41 | 42 | # 43 | ``` 44 | 45 | ## Extra configuration 46 | I've notice that during inverter startup or shotdown logger is power few times in a row (blinking display). This cause reset config cycle for Tasmota to prevent that set 47 | ``` 48 | SetOption65 1 49 | ``` 50 | 51 | ## Known issues 52 | SMI script doesn't integrate ideally with HA. I created an request to hatasmota library used to integrate with HA: https://github.com/emontnemery/hatasmota/issues/130 and that was a trigger to check ESPHome integration. Tasmota it's still usable and it's possible to configure with HA, but you need to add extra configs 53 | 54 | ## Image 55 | ![](../images/tasmota.png) -------------------------------------------------------------------------------- /tasmota_smi/user_config_override.h: -------------------------------------------------------------------------------- 1 | /* 2 | user_config_override.h - user configuration overrides my_user_config.h for Tasmota 3 | 4 | Copyright (C) 2021 Theo Arends 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | */ 19 | 20 | #ifndef _USER_CONFIG_OVERRIDE_H_ 21 | #define _USER_CONFIG_OVERRIDE_H_ 22 | 23 | /*****************************************************************************************************\ 24 | * USAGE: 25 | * To modify the stock configuration without changing the my_user_config.h file: 26 | * (1) copy this file to "user_config_override.h" (It will be ignored by Git) 27 | * (2) define your own settings below 28 | * 29 | ****************************************************************************************************** 30 | * ATTENTION: 31 | * - Changes to SECTION1 PARAMETER defines will only override flash settings if you change define CFG_HOLDER. 32 | * - Expect compiler warnings when no ifdef/undef/endif sequence is used. 33 | * - You still need to update my_user_config.h for major define USE_MQTT_TLS. 34 | * - All parameters can be persistent changed online using commands via MQTT, WebConsole or Serial. 35 | \*****************************************************************************************************/ 36 | 37 | 38 | #define USE_SCRIPT 39 | #define USE_SML_M 40 | #define SML_MAX_VARS 25 41 | #define SML_BSIZ 64 42 | 43 | #undef CODE_IMAGE_STR 44 | #define CODE_IMAGE_STR "small_sml" 45 | 46 | #undef USE_ARDUINO_OTA // Disable support for Arduino OTA 47 | #undef USE_DOMOTICZ // Disable Domoticz 48 | #define USE_HOME_ASSISTANT // Disable Home Assistant 49 | #undef USE_TASMOTA_DISCOVERY // Enable Tasmota Discovery support (+2k code) 50 | #undef USE_MQTT_TLS // Disable TLS support won't work as the MQTTHost is not set 51 | #undef USE_KNX // Disable KNX IP Protocol Support 52 | //#undef USE_WEBSERVER // Disable Webserver 53 | #undef USE_ENHANCED_GUI_WIFI_SCAN // Disable wifi scan output with BSSID (+0k5 code) 54 | #undef USE_WEBSEND_RESPONSE // Disable command WebSend response message (+1k code) 55 | #undef USE_EMULATION // Disable Wemo or Hue emulation 56 | #undef USE_EMULATION_HUE // Disable Hue Bridge emulation for Alexa (+14k code, +2k mem common) 57 | #undef USE_EMULATION_WEMO // Disable Belkin WeMo emulation for Alexa (+6k code, +2k mem common) 58 | #undef USE_CUSTOM // Disable Custom features 59 | #undef USE_DISCOVERY // Disable Discovery services for both MQTT and web server 60 | #undef USE_TIMERS // Disable support for up to 16 timers 61 | #undef USE_TIMERS_WEB // Disable support for timer webpage 62 | #undef USE_SUNRISE // Disable support for Sunrise and sunset tools 63 | #undef USE_RULES // Disable support for rules 64 | 65 | // -- Optional modules ------------------------- 66 | #undef ROTARY_V1 // Disable support for MI Desk Lamp 67 | #undef USE_SONOFF_RF // Disable support for Sonoff Rf Bridge (+3k2 code) 68 | #undef USE_RF_FLASH // Disable support for flashing the EFM8BB1 chip on the Sonoff RF Bridge. C2CK must be connected to GPIO4, C2D to GPIO5 on the PCB 69 | #undef USE_SONOFF_SC // Disable support for Sonoff Sc (+1k1 code) 70 | // #undef USE_TUYA_MCU // Disable support for Tuya Serial MCU 71 | #undef USE_ARMTRONIX_DIMMERS // Disable support for Armtronix Dimmers (+1k4 code) 72 | #undef USE_PS_16_DZ // Disable support for PS-16-DZ Dimmer and Sonoff L1 (+2k code) 73 | #undef USE_SONOFF_IFAN // Disable support for Sonoff iFan02 and iFan03 (+2k code) 74 | #undef USE_BUZZER // Disable support for a buzzer (+0k6 code) 75 | #undef USE_ARILUX_RF // Disable support for Arilux RF remote controller 76 | #undef USE_SHUTTER // Disable Shutter support for up to 4 shutter with different motortypes (+6k code) 77 | #undef USE_DEEPSLEEP // Disable support for deepsleep (+1k code) 78 | #undef USE_EXS_DIMMER // Disable support for EX-Store WiFi Dimmer 79 | #undef USE_HOTPLUG // Disable support for HotPlug 80 | #undef USE_DEVICE_GROUPS // Disable support for device groups (+3k5 code) 81 | #undef USE_PWM_DIMMER // Disable support for MJ-SD01/acenx/NTONPOWER PWM dimmers (+4k5 code) 82 | #undef USE_PWM_DIMMER_REMOTE // Disbale support for remote switches to PWM Dimmer 83 | #undef USE_KEELOQ // Disable support for Jarolift rollers by Keeloq algorithm (+4k5 code) 84 | #undef USE_SONOFF_D1 // Disable support for Sonoff D1 Dimmer (+0k7 code) 85 | 86 | // -- Optional light modules ---------------------- 87 | #undef USE_LIGHT_VIRTUAL_CT // Disable support for Virtual White Color Temperature (SO106) 88 | #undef USE_LIGHT // Also disable all Dimmer/Light support 89 | #undef USE_WS2812 // Disable WS2812 Led string using library NeoPixelBus (+5k code, +1k mem, 232 iram) - Disable by // 90 | #undef USE_MY92X1 // Disable support for MY92X1 RGBCW led controller as used in Sonoff B1, Ailight and Lohas 91 | #undef USE_SM16716 // Disable support for SM16716 RGB LED controller (+0k7 code) 92 | #undef USE_SM2135 // Disable support for SM2135 RGBCW led control as used in Action LSC (+0k6 code) 93 | #undef USE_SONOFF_L1 // Disable support for Sonoff L1 led control 94 | #undef USE_ELECTRIQ_MOODL // Disable support for ElectriQ iQ-wifiMOODL RGBW LED controller 95 | #undef USE_LIGHT_PALETTE // Disable support for color palette (+0k9 code) 96 | #undef USE_SHELLY_DIMMER // Disable support for Shelly Dimmer (+3k code) 97 | 98 | #undef USE_COUNTER // Disable counters 99 | #define USE_ADC_VCC // Display Vcc in Power status. Disable for use as Analog input on selected devices 100 | #undef USE_DS18x20 // Disable DS18x20 sensor 101 | #undef USE_I2C // Disable all I2C sensors and devices 102 | #undef USE_SPI // Disable all SPI devices 103 | #undef USE_DISPLAY // Disable Display support 104 | #undef USE_MHZ19 // Disable support for MH-Z19 CO2 sensor 105 | #undef USE_SENSEAIR // Disable support for SenseAir K30, K70 and S8 CO2 sensor 106 | #undef USE_PMS5003 // Disable support for PMS5003 and PMS7003 particle concentration sensor 107 | #undef USE_NOVA_SDS // Disable support for SDS011 and SDS021 particle concentration sensor 108 | #undef USE_HPMA // Disable support for Honeywell HPMA115S0 particle concentration sensor 109 | #undef USE_SR04 // Disable support for HC-SR04 ultrasonic devices (+1k code) 110 | #undef USE_DYP // Disable support for DYP ME-007 ultrasonic distance sensor, serial port version (+0k5 code) 111 | #undef USE_SERIAL_BRIDGE // Disable support for software Serial Bridge 112 | #undef USE_MP3_PLAYER // Disable DFPlayer Mini MP3 Player RB-DFR-562 commands: play, volume and stop 113 | #undef USE_AZ7798 // Disable support for AZ-Instrument 7798 CO2 datalogger 114 | #undef USE_PN532_HSU // Disable support for PN532 using HSU (Serial) interface (+1k8 code, 140 bytes mem) 115 | #undef USE_ZIGBEE // Disable serial communication with Zigbee CC2530 flashed with ZNP 116 | #undef USE_RDM6300 // Disable support for RDM6300 125kHz RFID Reader (+0k8) 117 | #undef USE_IBEACON // Disable support for bluetooth LE passive scan of ibeacon devices (uses HM17 module) 118 | #undef USE_GPS // Disable support for GPS and NTP Server for becoming Stratus 1 Time Source (+ 3.1kb flash, +132 bytes RAM) 119 | #undef USE_HM10 // (ESP8266 only) Disable support for HM-10 as a BLE-bridge for the LYWSD03 (+5k1 code) 120 | #undef USE_BLE_ESP32 // (ESP32 only) Disable support for native BLE on ESP32 - use new driver 121 | #undef USE_MI_ESP32 // (ESP32 only) Disable support for ESP32 as a BLE-bridge (+9k2 mem, +292k flash) 122 | #undef USE_HRXL // Disable support for MaxBotix HRXL-MaxSonar ultrasonic range finders (+0k7) 123 | #undef USE_TASMOTA_CLIENT // Disable support for Arduino Uno/Pro Mini via serial interface including flashing (+2k3 code, 44 mem) 124 | #undef USE_OPENTHERM // Disable support for OpenTherm (+15k code) 125 | #undef USE_MIEL_HVAC // Disable support for Mitsubishi Electric HVAC serial interface (+5k code) 126 | #undef USE_PROJECTOR_CTRL // Disable support for LCD/DLP Projector serial control interface 127 | 128 | #undef USE_ENERGY_SENSOR // Disable energy sensors 129 | #undef USE_PZEM004T // Disable PZEM004T energy sensor 130 | #undef USE_PZEM_AC // Disable PZEM014,016 Energy monitor 131 | #undef USE_PZEM_DC // Disable PZEM003,017 Energy monitor 132 | #undef USE_MCP39F501 // Disable MCP39F501 Energy monitor as used in Shelly 2 133 | #undef USE_SDM72 // Disable support for Eastron SDM72-Modbus energy meter 134 | #undef USE_SDM120 // Disable support for Eastron SDM120-Modbus energy meter 135 | #undef USE_SDM630 // Disable support for Eastron SDM630-Modbus energy monitor (+0k6 code) 136 | #undef USE_DDS2382 // Disable support for Hiking DDS2382 Modbus energy monitor (+0k6 code) 137 | #undef USE_DDSU666 // Disable support for Chint DDSU666 Modbus energy monitor (+0k6 code) 138 | #undef USE_SOLAX_X1 // Disable support for Solax X1 series Modbus log info (+3k1 code) 139 | #undef USE_LE01MR // Disable support for F&F LE-01MR Modbus energy meter (+2k code) 140 | #undef USE_TELEINFO // Disable support for French Energy Provider metering telemetry 141 | #undef USE_IEM3000 // Disable support for Schneider Electric iEM3000-Modbus series energy monitor (+0k8 code) 142 | #undef USE_WE517 // Disable support for Orno WE517-Modbus energy monitor (+1k code) 143 | 144 | #undef USE_DHT // Disable support for DHT11, AM2301 (DHT21, DHT22, AM2302, AM2321) and SI7021 Temperature and Humidity sensor 145 | #undef USE_MAX31855 // Disable MAX31855 K-Type thermocouple sensor using softSPI 146 | #undef USE_MAX31865 // Disable support for MAX31865 RTD sensors using softSPI 147 | #undef USE_IR_REMOTE // Disable IR driver 148 | 149 | #undef USE_TM1638 // Disable support for TM1638 switches copying Switch1 .. Switch8 150 | #undef USE_HX711 // Disable support for HX711 load cell 151 | #undef USE_TX20_WIND_SENSOR // Disable support for La Crosse TX20 anemometer 152 | #undef USE_TX23_WIND_SENSOR // Disable support for La Crosse TX23 anemometer 153 | #undef USE_WINDMETER // Disable support for analog anemometer (+2k2 code) 154 | #undef USE_RC_SWITCH // Disable support for RF transceiver using library RcSwitch 155 | #undef USE_RF_SENSOR // Disable support for RF sensor receiver (434MHz or 868MHz) (+0k8 code) 156 | #undef USE_HRE // Disable support for Badger HR-E Water Meter (+1k4 code) 157 | #undef USE_A4988_STEPPER // Disable support for A4988_Stepper 158 | #undef USE_THERMOSTAT // Disable support for Thermostat 159 | #undef DEBUG_THEO // Disable debug code 160 | #undef USE_DEBUG_DRIVER // Disable debug code 161 | #undef USE_AC_ZERO_CROSS_DIMMER // Disable support for AC_ZERO_CROSS_DIMMER 162 | 163 | 164 | #endif // _USER_CONFIG_OVERRIDE_H_ 165 | --------------------------------------------------------------------------------