├── pictures ├── esp32.gif ├── 20220806_205858.jpg ├── 20220806_205906.jpg ├── 20220806_205919.jpg ├── 20220806_205931.jpg ├── 20220806_205939.jpg ├── 20220806_210140.jpg └── MyVEDirectHardware.jpg ├── .gitignore ├── .vscode └── extensions.json ├── platformio.ini ├── LICENSE.md ├── src ├── vedirectSerial.h ├── mEEPROM.h ├── MQTT.h ├── ONEWIRE.h ├── mEEPROM.cpp ├── CANBUS.h ├── main.cpp ├── VEDirect.h ├── config.h └── CANBUS.cpp ├── HomeAssistant.yaml └── README.md /pictures/esp32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijones/VE.DirectMQTTCANBUS/HEAD/pictures/esp32.gif -------------------------------------------------------------------------------- /pictures/20220806_205858.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijones/VE.DirectMQTTCANBUS/HEAD/pictures/20220806_205858.jpg -------------------------------------------------------------------------------- /pictures/20220806_205906.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijones/VE.DirectMQTTCANBUS/HEAD/pictures/20220806_205906.jpg -------------------------------------------------------------------------------- /pictures/20220806_205919.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijones/VE.DirectMQTTCANBUS/HEAD/pictures/20220806_205919.jpg -------------------------------------------------------------------------------- /pictures/20220806_205931.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijones/VE.DirectMQTTCANBUS/HEAD/pictures/20220806_205931.jpg -------------------------------------------------------------------------------- /pictures/20220806_205939.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijones/VE.DirectMQTTCANBUS/HEAD/pictures/20220806_205939.jpg -------------------------------------------------------------------------------- /pictures/20220806_210140.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijones/VE.DirectMQTTCANBUS/HEAD/pictures/20220806_210140.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /pictures/MyVEDirectHardware.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijones/VE.DirectMQTTCANBUS/HEAD/pictures/MyVEDirectHardware.jpg -------------------------------------------------------------------------------- /.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 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /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 | [platformio] 12 | default_envs = esp32dev 13 | data_dir = ./data 14 | src_dir = ./src 15 | 16 | [env:esp32dev] 17 | platform = espressif32 18 | board = esp32dev 19 | board_build.filesystem = littlefs 20 | framework = arduino 21 | lib_deps = 22 | Time 23 | https://github.com/plapointe6/EspMQTTClient.git 24 | https://github.com/arduino-libraries/Arduino_JSON.git 25 | https://github.com/PaulStoffregen/Time.git 26 | https://github.com/me-no-dev/ESPAsyncTCP.git 27 | https://github.com/aharshac/StringSplitter.git 28 | https://github.com/coryjfowler/MCP_CAN_lib.git 29 | upload_protocol = esptool 30 | monitor_speed = 115200 31 | ;upload_port = COM3 32 | build_type = debug 33 | build_flags = -DCORE_DEBUG_LEVEL=3 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ralf Lehmann 4 | 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /src/vedirectSerial.h: -------------------------------------------------------------------------------- 1 | /* 2 | VE.Direct Serial code. 3 | 4 | GITHUB Link 5 | 6 | MIT License 7 | 8 | Copyright (c) 2020 Ralf Lehmann 9 | 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | */ 29 | 30 | #ifndef VEDIRECTSERIAL_H 31 | #define VEDIRECTSERIAL_H 32 | 33 | void startVEDirectSerial() { 34 | Serial1.begin(19200, SERIAL_8N1, VEDIRECT_RX, VEDIRECT_TX); 35 | } 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /HomeAssistant.yaml: -------------------------------------------------------------------------------- 1 | # Home Assistant MQTT config for VE.DirectMQTTCANBUS, allows switch control of charge discharge 2 | # and force charging of the batteries - useful for off peak charging 3 | mqtt: 4 | switch: 5 | - name: "PylonTech Protocol" # Choose an easy-to-recognize name 6 | unique_id: "PylontechProtocol" 7 | state_topic: "SMARTBMS/Param/EnablePYLONTECH" # Topic to read the current state 8 | command_topic: "SMARTBMS/set/EnablePYLONTECH" # Topic to publish commands 9 | qos: 1 10 | payload_on: "ON" # or "on", depending on your MQTT device 11 | payload_off: "OFF" # or "off", depending on your MQTT device 12 | retain: true # or false if you want to wait for changes 13 | 14 | - name: "Force Charge" # Choose an easy-to-recognize name 15 | unique_id: "ForceCharge" 16 | state_topic: "SMARTBMS/Param/ForceCharge" # Topic to read the current state 17 | command_topic: "SMARTBMS/set/ForceCharge" # Topic to publish commands 18 | qos: 1 19 | payload_on: "ON" # or "on", depending on your MQTT device 20 | payload_off: "OFF" # or "off", depending on your MQTT device 21 | retain: true # or false if you want to wait for changes 22 | 23 | - name: "Discharge Enable" # Choose an easy-to-recognize name 24 | unique_id: "DischargeEnable" 25 | state_topic: "SMARTBMS/Param/DischargeEnable" # Topic to read the current state 26 | command_topic: "SMARTBMS/set/DischargeEnable" # Topic to publish commands 27 | qos: 1 28 | payload_on: "ON" # or "on", depending on your MQTT device 29 | payload_off: "OFF" # or "off", depending on your MQTT device 30 | retain: true # or false if you want to wait for changes 31 | 32 | - name: "Charge Enable" # Choose an easy-to-recognize name 33 | unique_id: "ChargeEnable" 34 | state_topic: "SMARTBMS/Param/ChargeEnable" # Topic to read the current state 35 | command_topic: "SMARTBMS/set/ChargeEnable" # Topic to publish commands 36 | qos: 1 37 | payload_on: "ON" # or "on", depending on your MQTT device 38 | payload_off: "OFF" # or "off", depending on your MQTT device 39 | retain: true # or false if you want to wait for changes 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This code will no longer be maintained, please use https://github.com/sijones/DiyBatteryBMS.git 2 | 3 | VE.Direct2MQTTCANBUS takes data from a Victron Smart Shunt and sends it to a inverter over CAN allowing for "DIY LifePO4" Batteries to be integrated. 4 | 5 | The data is also sent over MQTT and allows commands to be sent back to control Charge/Discharge/Force Charge. 6 | 7 | This software uses a ESP32 developers board with a MCP2515 Can Bus adapter currently developed using Visual Studio code. 8 | 9 | The software sends the data in Pylontech Protocol, most inverters should support this. 10 | 11 | - See also: https://www.victronenergy.com/live/vedirect_protocol:faq 12 | 13 | With the help of the MQTT server you can integrate the monitoring data to virtually any Home Automation System. I use Home Assistant to automate off peak battery charging (using Force Charge) and can also enable and disable the charging and discharging. 14 | 15 | ## Features 16 | - Listen to VE.Direct messages and publish a block (consisting of several key-value pairs) to a MQTT broker
Every key from the device will be appended to the MQTT_PREFIX and build a topic. e.g. MQTT_PREFIX="/SMARTBMS"; Topic /SMARTBMS/V will contain the Battery Voltage
so please see the VE.Direct protocol for the meaning of topics 17 | - Supports MQTT Commands to enable and disable charge/discharging of an inverter, force charge the batteries to be able to charge over night at off peak rates. See the home assistant file for the commands and config. 18 | - SSL is currently disabled 19 | - Supports single MQTT server 20 | - OneWire temperature sensors will be supported in a future version 21 | - OTA (Over The Air Update)
use your browser and go to http://IPADDRESS/ota and upload the lastest binary. 22 | - One config file to enable/disable features and configure serial port or MQTT Topics 23 | - Works with both Victron Smart Shunt and BMV hardware 24 | 25 | ## Limitations 26 | - VE.Direct2MQTT is only listening to messages of the VE.Direct device
It understands only the "ASCII" part of the protocol that is only good to receive a set of values. You can't request any special data or change any parameters of the VE.Direct device.
27 | 28 | ## Hardware & Software Installation 29 | See the Wiki page 30 | 31 | ## Disclaimer 32 | I WILL NOT BE HELD LIABLE FOR ANY DAMAGE THAT YOU DO TO YOU OR ONE OF YOUR DEVICES. 33 | -------------------------------------------------------------------------------- /src/mEEPROM.h: -------------------------------------------------------------------------------- 1 | #ifndef VEDIRECTEEPROM_H 2 | #define VEDIRECTEEPROM_H 3 | 4 | // Constants 5 | // no key larger than 15 chars 6 | 7 | #include 8 | #define RW_MODE true 9 | #define RO_MODE false 10 | 11 | const char* const ccChargeVolt = "ChargeVolt"; 12 | const char* const ccDischargeVolt = "DischargeVolt"; 13 | const char* const ccChargeCurrent = "ChargeCurr"; 14 | const char* const ccDischargeCurrent = "DischargeCurr"; 15 | 16 | const char* const ccLowSOCLimit = "LowSOCLimit"; 17 | const char* const ccHighSOCLimit = "HighSOCLimit"; 18 | 19 | const char* const ccBattCapacity = "BattCapacity"; 20 | const char* const ccPylonTech = "PylonTech"; 21 | 22 | const char* const ccWifiSSID = "WifiSSID"; 23 | const char* const ccWifiPass = "WifiPass"; 24 | const char* const ccWifiHostName = "WifiHostName"; 25 | 26 | const char* const ccMQTTServerIP = "MQTTServerIP"; 27 | const char* const ccMQTTClientID = "MQTTClientID"; 28 | const char* const ccMQTTUser = "MQTTUser"; 29 | const char* const ccMQTTPass = "MQTTPass"; 30 | const char* const ccMQTTPort = "MQTTPort"; 31 | const char* const ccMQTTTopic = "MQTTTopic"; 32 | const char* const ccMQTTParam = "MQTTParam"; 33 | 34 | const char* const ccVictronRX = "VictronRX"; 35 | const char* const ccVictronTX = "VictronTX"; 36 | const char* const ccCanCSPin = "CAN_CS_PIN"; 37 | 38 | const char* const ccVELOOPTIME = "VE_LOOP_TIME"; 39 | 40 | const char* const PREF_NAME = "smartbms"; 41 | 42 | 43 | class mEEPROM { 44 | public: 45 | mEEPROM(); 46 | void begin(); 47 | void end(); 48 | 49 | Preferences _preferences; 50 | boolean isKey(String key); 51 | boolean clear(); 52 | String getString(String key, String default_value); 53 | String getString(const char* key, String default_value); 54 | String getString(int key, String default_value); 55 | boolean putString(String key, String value); 56 | boolean putString(const char* key, String value); 57 | boolean putString(int key, String value); 58 | int32_t getInt(int key, int default_value); 59 | int32_t getInt(String key, int default_value); 60 | boolean putInt(int key, int32_t value); 61 | boolean putInt(String key, int32_t value); 62 | uint32_t getUInt(uint32_t key, uint32_t default_value); 63 | uint32_t getUInt(String key, uint32_t default_value); 64 | boolean putUInt(uint32_t key, uint32_t value); 65 | boolean putUInt(String key, uint32_t value); 66 | boolean getBool(int key, boolean default_value); 67 | boolean getBool(String key, boolean default_value); 68 | boolean putBool(int key, boolean value); 69 | boolean putBool(String key, boolean value); 70 | 71 | }; 72 | 73 | 74 | #endif 75 | -------------------------------------------------------------------------------- /src/MQTT.h: -------------------------------------------------------------------------------- 1 | void onMessageReceived(const String& topic, const String& message) 2 | { 3 | if (topic == MQTT_PREFIX + "/set/" + "DischargeCurrent") { 4 | Inverter.SetDischargeCurrent((uint32_t) message.toInt()); 5 | client.publish(MQTT_PREFIX + "/Param/DischargeCurrent", message); 6 | } 7 | else if (topic == MQTT_PREFIX + "/set/" + "ChargeVoltage") { 8 | if (message.toInt() > 0) { 9 | Inverter.SetChargeVoltage((uint32_t) message.toInt()); 10 | client.publish(MQTT_PREFIX + "/Param/ChargeVoltage", message); 11 | } 12 | } 13 | else if (topic == MQTT_PREFIX + "/set/" + "ChargeCurrent") { 14 | Inverter.SetChargeCurrent((uint32_t) message.toInt()); 15 | client.publish(MQTT_PREFIX + "/Param/ChargeCurrent", message); 16 | } 17 | else if (topic == MQTT_PREFIX + "/set/" + "ForceCharge") { 18 | Inverter.ForceCharge((message == "ON") ? true : false); 19 | client.publish(MQTT_PREFIX + "/Param/ForceCharge", (Inverter.ForceCharge() == true) ? "ON" : "OFF" ); } 20 | else if (topic == MQTT_PREFIX + "/set/" + "DischargeEnable") { 21 | Inverter.DischargeEnable((message == "ON") ? true : false); 22 | client.publish(MQTT_PREFIX + "/Param/DischargeEnable", (Inverter.DischargeEnable() == true) ? "ON" : "OFF" ); } 23 | else if (topic == MQTT_PREFIX + "/set/" + "ChargeEnable") { 24 | Inverter.ChargeEnable((message == "ON") ? true : false); 25 | client.publish(MQTT_PREFIX + "/Param/ChargeEnable", (Inverter.ChargeEnable() == true) ? "ON" : "OFF" ); } 26 | else if (topic == MQTT_PREFIX + "/set/" + "EnablePYLONTECH") { 27 | Inverter.EnablePylonTech((message == "ON") ? true : false); 28 | client.publish(MQTT_PREFIX + "/Param/EnablePYLONTECH", (Inverter.EnablePylonTech() == true) ? "ON" : "OFF" ); } 29 | else { 30 | client.publish(MQTT_PREFIX + "/LastMessage", "Command not recognised, Topic: " + topic + " - Payload: " + message); 31 | } 32 | } 33 | 34 | // 35 | // Send ASCII data from passive mode to MQTT 36 | // 37 | bool sendASCII2MQTT(VEDirectBlock_t * block) { 38 | for (int i = 0; i < block->kvCount; i++) { 39 | String key = block->b[i].key; 40 | String value = block->b[i].value; 41 | String topic = MQTT_PREFIX + "/" + key; 42 | if (client.isMqttConnected()) { 43 | topic.replace("#", ""); // # in a topic is a no go for MQTT 44 | value.replace("\r\n", ""); 45 | if ( client.publish(topic.c_str(), value.c_str())) { 46 | log_d("MQTT message sent succesfully: %s: \"%s\"", topic.c_str(), value.c_str()); 47 | } else { 48 | log_e("Sending MQTT message failed: %s: %s", topic.c_str(), value.c_str()); 49 | } 50 | } 51 | } 52 | return true; 53 | } 54 | 55 | bool sendUpdateMQTTData() 56 | { 57 | if (client.isMqttConnected()){ 58 | client.publish(MQTT_PREFIX + "/Param/EnablePYLONTECH", (Inverter.EnablePylonTech() == true) ? "ON" : "OFF" ); 59 | client.publish(MQTT_PREFIX + "/Param/ForceCharge", (Inverter.ForceCharge() == true) ? "ON" : "OFF" ); 60 | client.publish(MQTT_PREFIX + "/Param/DischargeEnable", (Inverter.DischargeEnable() == true) ? "ON" : "OFF" ); 61 | client.publish(MQTT_PREFIX + "/Param/ChargeEnable", (Inverter.ChargeEnable() == true) ? "ON" : "OFF" ); 62 | return true; 63 | } else 64 | return false; 65 | } 66 | 67 | void onConnectionEstablished() 68 | { 69 | log_i("Wifi / MQTT Connected"); 70 | client.subscribe(MQTT_PREFIX + "/set/#", onMessageReceived); 71 | } 72 | -------------------------------------------------------------------------------- /src/ONEWIRE.h: -------------------------------------------------------------------------------- 1 | /* 2 | VE.Direct OneWire temperature sensors code. 3 | 4 | GITHUB Link 5 | 6 | MIT License 7 | 8 | Copyright (c) 2020 Ralf Lehmann 9 | 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | */ 29 | 30 | #ifndef VEDIRECTONEWIRE_H 31 | #define VEDIRECTONEWIRE_H 32 | 33 | #include 34 | #include 35 | 36 | #define MAX_DS18SENSORS 3 37 | #define MAX_MEASSUREMENTS 3 38 | 39 | OneWire oneWire(ONEWIRE_PIN); 40 | DallasTemperature sensors(&oneWire); 41 | int deviceCount = 0; 42 | boolean onewire_good_values = false; 43 | 44 | String addr2String(DeviceAddress addr) { 45 | String s; 46 | for (int i = 0; i < 7; i++) { 47 | s += String(addr[i]) + ":"; 48 | } 49 | return s += addr[7]; 50 | } 51 | 52 | boolean meassureOneWire() { 53 | int m_count = MAX_MEASSUREMENTS; 54 | do { 55 | m_count --; 56 | onewire_good_values = true; 57 | sensors.begin(); 58 | sensors.setWaitForConversion(true); 59 | delay(1000); 60 | sensors.requestTemperatures(); 61 | delay(1000); 62 | deviceCount = sensors.getDeviceCount(); 63 | for (int i = 0; i < deviceCount; i++) { 64 | float temp = sensors.getTempCByIndex(i); 65 | if ( temp > 200 || temp < -80 ) { 66 | onewire_good_values = false; 67 | } 68 | } 69 | } while (m_count >= 0 && !onewire_good_values); 70 | } 71 | 72 | boolean sendOneWireMQTT() { 73 | meassureOneWire(); 74 | 75 | log_d("Found %d One wire temp sensors", deviceCount); 76 | float c[deviceCount]; 77 | 78 | if ( !onewire_good_values || deviceCount <= 0) { 79 | return false; 80 | } 81 | 82 | for (int i = 0; i < deviceCount; i++) { 83 | DeviceAddress addr; 84 | sensors.getAddress((uint8_t *)&addr, (uint8_t) i); 85 | c[i] = sensors.getTempCByIndex(i); 86 | log_d("DS18: %s, Temp: %f", addr2String(addr).c_str(), c[i]); 87 | } 88 | 89 | if ( !espMQTT.connected()) { 90 | startMQTT(); 91 | } 92 | 93 | StaticJsonDocument<300> doc; 94 | // Add values in the document 95 | // 96 | int count = deviceCount > MAX_DS18SENSORS ? MAX_DS18SENSORS : deviceCount; 97 | log_d("Sending: %d Devices", count); 98 | for (int i = 0; i < count; i++) { 99 | DeviceAddress addr; 100 | sensors.getAddress((uint8_t *)&addr, (uint8_t) i); 101 | String s = addr2String(addr); 102 | doc[s] = c[i]; 103 | } char s[300]; 104 | serializeJson(doc, s); 105 | if ( espMQTT.publish(MQTT_ONEWIRE.c_str(), s)) { 106 | log_d("Sending OneWire Data: %s - OK", s); 107 | } else { 108 | log_d("Sending OneWire %s Data: %s - ERROR", MQTT_ONEWIRE.c_str(), s); 109 | } 110 | espMQTT.loop(); 111 | if ( mqtt_param_rec ) { 112 | // avoid loops by sending only if we received a valid parameter 113 | log_i("Removing parameter from Queue: %s", MQTT_PARAMETER.c_str()); 114 | espMQTT.publish(MQTT_PARAMETER.c_str(), "", true); 115 | } 116 | return true; 117 | } 118 | 119 | 120 | #endif 121 | -------------------------------------------------------------------------------- /src/mEEPROM.cpp: -------------------------------------------------------------------------------- 1 | #include "mEEPROM.h" 2 | 3 | mEEPROM::mEEPROM() { 4 | // Open Preferences with my-app namespace. Each application module, library, etc 5 | // has to use a namespace name to prevent key name collisions. We will open storage in 6 | // RW-mode (second parameter has to be false). 7 | // Note: Namespace name is limited to 15 chars. 8 | 9 | } 10 | 11 | void mEEPROM::begin() { 12 | if (!_preferences.begin(PREF_NAME)) 13 | log_e("Failed to Open EEPROM in RW Mode for settings retrival."); 14 | _preferences.end(); 15 | } 16 | 17 | void mEEPROM::end() { 18 | 19 | } 20 | 21 | boolean mEEPROM::isKey(String key){ 22 | bool exists; 23 | _preferences.begin(PREF_NAME); 24 | exists = _preferences.isKey(key.c_str()); 25 | _preferences.end(); 26 | return exists; 27 | } 28 | 29 | boolean mEEPROM::clear() 30 | { 31 | bool _cleared; 32 | _preferences.begin(PREF_NAME); 33 | _cleared = _preferences.clear(); 34 | _preferences.end(); 35 | return _cleared; 36 | } 37 | 38 | int32_t mEEPROM::getInt(String key, int default_value = 0) { 39 | _preferences.begin(PREF_NAME); 40 | int32_t ret = _preferences.getInt(key.c_str(), default_value); 41 | log_d("PrefGetInt; \'%s\' = \'%d\'", key.c_str(), ret); 42 | _preferences.end(); 43 | return ret; 44 | } 45 | 46 | boolean mEEPROM::putInt(String key, int32_t value) { 47 | _preferences.begin(PREF_NAME); 48 | _preferences.putInt(key.c_str(), value); 49 | log_d("PrefPutInt; \'%s\' = \'%d\'", key.c_str(), value); 50 | _preferences.end(); 51 | return true; 52 | } 53 | 54 | int32_t mEEPROM::getInt(int key, int default_value = 0) { 55 | return mEEPROM::getInt(String(key), default_value); 56 | } 57 | 58 | boolean mEEPROM::putInt(int key, int32_t value = 0) { 59 | return mEEPROM::putInt(String(key), value); 60 | } 61 | 62 | uint32_t mEEPROM::getUInt(String key, uint32_t default_value = 0) { 63 | _preferences.begin(PREF_NAME); 64 | uint32_t ret = _preferences.getUInt(key.c_str(), default_value); 65 | log_d("PrefGetInt; \'%s\' = \'%d\'", key.c_str(), ret); 66 | _preferences.end(); 67 | return ret; 68 | } 69 | 70 | boolean mEEPROM::putUInt(String key, uint32_t value) { 71 | _preferences.begin(PREF_NAME); 72 | _preferences.putUInt(key.c_str(), value); 73 | log_d("PrefPutInt; \'%s\' = \'%d\'", key.c_str(), value); 74 | _preferences.end(); 75 | return true; 76 | } 77 | 78 | uint32_t mEEPROM::getUInt(uint32_t key, uint32_t default_value = 0) { 79 | return mEEPROM::getUInt(String(key), default_value); 80 | } 81 | 82 | boolean mEEPROM::putUInt(uint32_t key, uint32_t value = 0) { 83 | return mEEPROM::putUInt(String(key), value); 84 | } 85 | 86 | String mEEPROM::getString(int key, String default_value = String("")){ 87 | return getString(String(key), default_value); 88 | } 89 | 90 | String mEEPROM::getString(String key, String default_value = String("")) { 91 | _preferences.begin(PREF_NAME); 92 | String ret = _preferences.getString(key.c_str(), default_value.c_str()); 93 | 94 | log_d("PrefGetStr: \'%s\' = \'%s\'", key.c_str(), ret.c_str()); 95 | _preferences.end(); 96 | return ret; 97 | } 98 | 99 | String mEEPROM::getString(const char* key, String default_value = String("")) { 100 | _preferences.begin(PREF_NAME); 101 | String ret = _preferences.getString(key, default_value.c_str()); 102 | log_d("PrefGetStr: \'%s\' = \'%s\'", key, ret.c_str()); 103 | _preferences.end(); 104 | return ret; 105 | } 106 | 107 | boolean mEEPROM::putString(int key, String value){ 108 | return putString(String(key), value); 109 | } 110 | 111 | boolean mEEPROM::putString(String key, String value) { 112 | _preferences.begin(PREF_NAME); 113 | _preferences.putString(key.c_str(), value); 114 | log_d("PrefputStr: \'%s\' = \'%s\'", key.c_str(), value); 115 | _preferences.end(); 116 | return true; 117 | } 118 | 119 | boolean mEEPROM::putString(const char* key, String value) { 120 | _preferences.begin(PREF_NAME); 121 | _preferences.putString(key, value); 122 | log_d("PrefputStr: \'%s\' = \'%s\'", key, value); 123 | _preferences.end(); 124 | return true; 125 | } 126 | 127 | // Boolean 128 | boolean mEEPROM::getBool(String key, boolean default_value = false) { 129 | _preferences.begin(PREF_NAME); 130 | boolean ret = _preferences.getBool(key.c_str(), default_value); 131 | log_d("PrefGetInt; \'%s\' = \'%d\'", key.c_str(), ret); 132 | _preferences.end(); 133 | return ret; 134 | } 135 | 136 | boolean mEEPROM::putBool(String key, boolean value) { 137 | _preferences.begin(PREF_NAME); 138 | _preferences.putBool(key.c_str(), value); 139 | log_d("PrefPutInt; \'%s\' = \'%d\'", key.c_str(), value); 140 | _preferences.end(); 141 | return true; 142 | } 143 | 144 | boolean mEEPROM::getBool(int key, boolean default_value = 0) { 145 | return mEEPROM::getInt(String(key), default_value); 146 | } 147 | 148 | boolean mEEPROM::putBool(int key, boolean value = false) { 149 | return mEEPROM::putInt(String(key), value); 150 | } -------------------------------------------------------------------------------- /src/CANBUS.h: -------------------------------------------------------------------------------- 1 | /* 2 | PYLON Protocol, messages sent every 1 second. 3 | 4 | 0x351 – 14 02 74 0E 74 0E CC 01 – Battery voltage + current limits 5 | 0x355 – 1A 00 64 00 – State of Health (SOH) / State of Charge (SOC) 6 | 0x356 – 4e 13 02 03 04 05 – Voltage / Current / Temp 7 | 0x359 – 00 00 00 00 0A 50 4E – Protection & Alarm flags 8 | 0x35C – C0 00 – Battery charge request flags 9 | 0x35E – 50 59 4C 4F 4E 20 20 20 – Manufacturer name (“PYLON “) 10 | 11 | */ 12 | 13 | #include 14 | #include 15 | #include // Library for CAN Interface https://github.com/coryjfowler/MCP_CAN_lib 16 | #include "mEEPROM.h" 17 | 18 | class CANBUS { 19 | private: 20 | //#pragma once 21 | //#define CAN_INT 22 //CAN Init Pin for M5Stack 22 | //#define CAN_CS_PIN 2 //CAN CS PIN 23 | 24 | #include // Library for CAN Interface https://github.com/coryjfowler/MCP_CAN_lib 25 | #include 26 | 27 | // CAN BUS Library 28 | MCP_CAN *CAN; 29 | uint8_t CAN_MSG[7]; 30 | uint8_t MSG_PYLON[8] = {0x50,0x59,0x4C,0x4F,0x4E,0x20,0x20,0x20}; 31 | 32 | /* 33 | uint8_t lowByte; 34 | uint8_t highByte; 35 | */ 36 | bool _initialised = false; 37 | bool _enablePYLONTECH = false; 38 | bool _forceCharge = false; 39 | bool _chargeEnabled = true; 40 | bool _dischargeEnabled = true; 41 | bool _dataChanged = false; 42 | uint8_t _canSendDelay = 5; 43 | 44 | enum Charging { 45 | bmsForceCharge = 8, 46 | bmsDischargeEnable = 64, 47 | bmsChargeEnable = 128 48 | }; 49 | // Flags set to check all data has come before starting CANBUS sending 50 | bool _initialBattSOC = false; 51 | bool _initialBattVoltage = false; 52 | bool _initialBattCurrent = false; 53 | bool _initialChargeVoltage = false; 54 | bool _initialChargeCurrent = false; 55 | bool _initialDischargeVoltage = false; 56 | bool _initialDischargeCurrent = false; 57 | bool _initialDone = false; 58 | bool _initialConfig = false; 59 | bool _initialBattData = false; 60 | 61 | // Used to tell the inverter battery data 62 | volatile uint8_t _battSOC = 0; 63 | volatile uint8_t _battSOH = 100; // State of health, not useful so defaulted to 100% 64 | volatile uint16_t _battVoltage = 0; 65 | volatile int32_t _battCurrentmA = 0; 66 | volatile int16_t _battTemp = 10; 67 | uint32_t _battCapacity = 0; // Only used for limiting current at high SOC. 68 | 69 | // Used to tell the inverter battery limits 70 | volatile uint32_t _chargeVoltage = 0; 71 | volatile uint32_t _dischargeVoltage = 0; 72 | volatile uint32_t _chargeCurrentmA = 0; 73 | volatile uint32_t _dischargeCurrentmA = 0; 74 | 75 | // These are set by the initial call to set Current Limits and used as the max. 76 | uint32_t _maxChargeCurrentmA = 0; 77 | uint32_t _maxDischargeCurrentmA = 0; 78 | 79 | // Track how many failed CAN BUS sends and reboot ESP if more than limit 80 | uint8_t _maxFailedCanSendCount = 20; 81 | uint8_t _failedCanSendCount = 0; 82 | 83 | uint32_t LoopTimer; // store current time 84 | // The interval for sending the inverter updated information 85 | // Normally around 5 seconds is ok, but for Pylontech protocol it's around every second. 86 | uint16_t _CanBusSendInterval = 1000; 87 | // Task Handle 88 | TaskHandle_t tHandle = NULL; 89 | 90 | public: 91 | 92 | enum Command 93 | { 94 | ChargeDischargeLimits = 0x351, 95 | BattVoltCurrent = 0x356, 96 | StateOfCharge = 0x355 97 | }; 98 | 99 | //void CANBUSBMS(); 100 | bool Begin(uint8_t _CS_PIN); 101 | bool SendBattUpdate(uint8_t SOC, uint16_t Voltage, int32_t CurrentmA, int16_t BattTemp, uint8_t SOH); 102 | bool SendAllUpdates(); 103 | bool SendBattUpdate(); 104 | bool SendParamUpdate(); 105 | bool DataChanged(); 106 | void SetChargeVoltage(uint32_t Voltage); 107 | void SetChargeCurrent(uint32_t CurrentmA); 108 | void SetDischargeVoltage(uint32_t Voltage); 109 | void SetDischargeCurrent(uint32_t CurrentmA); 110 | void ChargeEnable(bool); 111 | void DischargeEnable(bool); 112 | bool AllReady(); 113 | void ForceCharge(bool); 114 | bool Initialised(){return _initialised;} 115 | bool Configured(); 116 | 117 | 118 | void BattSOC(uint8_t soc){_initialBattSOC = true; _battSOC = soc;} 119 | void BattVoltage(uint16_t voltage){_initialBattVoltage = true; _battVoltage = voltage;} 120 | void BattSOH(uint8_t soh){_battSOH = soh;} 121 | void BattCurrentmA(int32_t currentmA){_initialBattCurrent = true; _battCurrentmA = currentmA;} 122 | void BattTemp(int16_t batttemp){_battTemp = batttemp;} 123 | void SetBattCapacity(uint32_t BattCapacity){_battCapacity = BattCapacity;} 124 | void EnablePylonTech(bool State); 125 | 126 | uint8_t BattSOC(){return _battSOC;} 127 | uint16_t BattVoltage(){return _battVoltage;} 128 | uint8_t BattSOH(){return _battSOH;} 129 | int32_t BattCurrentmA(){return _battCurrentmA;} 130 | int16_t BattTemp(){return _battTemp;} 131 | bool ForceCharge(){return _forceCharge;} 132 | bool ChargeEnable(){return _chargeEnabled;} 133 | bool DischargeEnable(){return _dischargeEnabled;} 134 | bool EnablePylonTech(){return _enablePYLONTECH;} 135 | bool CanBusFailed(){return _failedCanSendCount > _maxFailedCanSendCount;} 136 | mEEPROM pref; 137 | 138 | }; // End of Class 139 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | VE.Direct to CAN BUS & MQTT Gateway using a ESP32 Board 4 | Collect Data from VE.Direct device like Victron MPPT 75/15 / Smart Shunt 5 | and send it to a an inverter and MQTT gateway. From there you can 6 | integrate the data into any Home Automation Software like 7 | ioBroker annd make graphs. 8 | 9 | The ESP32 will read data from the VE.Direct interface and transmit the 10 | data via WiFi to a MQTT broker and via CAN Bus to an inverter, it supports 11 | the basic profile and Pylontech protocol. 12 | 13 | GITHUB Link 14 | 15 | MIT License 16 | 17 | Copyright (c) 2020 Ralf Lehmann 18 | 19 | 20 | Copyright (c) 2022 Simon Jones 21 | 22 | Implemented new Wifi & MQTT Library supporting OTA on device no web server is required 23 | use: http://IPAddress/OTA and browse to the bin file and update 24 | Implemented CAN Bus support using MCP2515 chip and library from: 25 | 26 | 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining a copy 29 | of this software and associated documentation files (the "Software"), to deal 30 | in the Software without restriction, including without limitation the rights 31 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 | copies of the Software, and to permit persons to whom the Software is 33 | furnished to do so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in all 36 | copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 | SOFTWARE. 45 | */ 46 | 47 | 48 | /* 49 | All configuration comes from config.h 50 | So please see there for WiFi, MQTT and OTA configuration 51 | */ 52 | #include 53 | #include "FS.h" 54 | #include "SPIFFS.h" 55 | #include "config.h" 56 | 57 | #include "EspMQTTClient.h" 58 | #include "mEEPROM.h" 59 | mEEPROM pref; 60 | 61 | EspMQTTClient client( 62 | ssid, 63 | pw, 64 | mqtt_server, 65 | mqtt_username, // Omit this parameter to disable client authentification 66 | mqtt_password, // Omit this parameter to disable client authentification 67 | mqtt_clientID, 68 | mqtt_port); 69 | 70 | #include "TimeLib.h" 71 | #include "VEDirect.h" 72 | #include "CANBUS.h" 73 | uint32_t SendCanBusMQTTUpdates; 74 | CANBUS Inverter; 75 | 76 | #include "MQTT.h" 77 | 78 | #ifdef USE_ONEWIRE 79 | #include "ONEWIRE.h" 80 | #endif 81 | 82 | time_t last_boot; 83 | VEDirect ve; 84 | //time_t last_vedirect; 85 | uint32_t last_vedirect_millis; 86 | 87 | void UpdateCanBusData(VEDirectBlock_t * block) { 88 | for (int i = 0; i < block->kvCount; i++) { 89 | bool dataValid = false; 90 | String key = block->b[i].key; 91 | String value = block->b[i].value; 92 | String parsedValue = ""; 93 | if (value.startsWith("-")) 94 | parsedValue = "-"; 95 | 96 | for (auto x : value) 97 | { 98 | if (isDigit(x)) 99 | parsedValue += x; 100 | } 101 | if (parsedValue.length() > 0) 102 | dataValid = true; 103 | 104 | //int intValue = parsedValue.toInt(); 105 | //if ( espMQTT.publish(topic.c_str(), value.c_str())) { 106 | // log_i("MQTT message sent succesfully: %s: \"%s\"", topic.c_str(), value.c_str()); 107 | //} else { 108 | // log_e("Sending MQTT message failed: %s: %s", topic.c_str(), value.c_str()); 109 | //} 110 | 111 | if (key.compareTo(String('V')) == 0) 112 | { 113 | log_i("Battery Voltage Update: %sV", parsedValue.c_str()); 114 | if (dataValid) Inverter.BattVoltage((uint16_t) round(parsedValue.toInt() * 0.1)); 115 | } 116 | 117 | if (key.compareTo(String('I')) == 0) 118 | { 119 | log_i("Battery Current Update: %smA",parsedValue.c_str()); 120 | if (dataValid) Inverter.BattCurrentmA((int32_t) (parsedValue.toInt() *0.01 )); 121 | } 122 | 123 | if (key.compareTo(String("SOC")) == 0) 124 | { 125 | log_i("Battery SOC Update: %s%%",parsedValue.c_str()); 126 | if (dataValid) Inverter.BattSOC((uint8_t) round((parsedValue.toInt()*0.1))); 127 | } 128 | 129 | /* if (key.compareTo(String('SOC')) == 0) 130 | { 131 | log_i("Battery Temp Update: %sC",parsedValue.c_str()); 132 | BattTemp((uint16_t) (parsedValue.toInt()*0.1)); 133 | } */ 134 | 135 | 136 | } 137 | } 138 | 139 | void setup() { 140 | Serial.begin(115200); 141 | 142 | #ifdef USE_OTA 143 | client.enableHTTPWebUpdater("/ota"); 144 | client.enableOTA(mqtt_password,8266); 145 | #endif 146 | 147 | if (Inverter.Begin(CAN_CS_PIN)) { 148 | Inverter.SetChargeVoltage(initBattChargeVoltage); 149 | Inverter.SetChargeCurrent(initBattChargeCurrent); 150 | Inverter.SetDischargeVoltage(initBattDischargeVoltage); 151 | Inverter.SetDischargeCurrent(initBattDischargeCurrent); 152 | Inverter.SetBattCapacity(initBattCapacity); 153 | #ifdef USE_PYLONTECH 154 | Inverter.EnablePylonTech(true); 155 | #endif 156 | SendCanBusMQTTUpdates = millis(); 157 | last_vedirect_millis = millis(); 158 | ve.begin(); 159 | // looking good; moving to loop 160 | return; 161 | } 162 | // } 163 | // oh oh, we did not get CANBUS, that is bad; we can't continue 164 | // wait a while and reboot to try again 165 | delay(5000); 166 | ESP.restart(); 167 | } 168 | 169 | void loop() { 170 | VEDirectBlock_t block; 171 | //time_t t = time(nullptr); 172 | // MQTT Processing loop 173 | client.loop(); 174 | 175 | #ifdef USE_ONEWIRE 176 | if ( abs(t - last_ow) >= OW_WAIT_TIME) { 177 | if ( checkWiFi()) { 178 | sendOneWireMQTT(); 179 | last_ow = t; 180 | //sendOPInfo(); 181 | } 182 | } 183 | #endif 184 | 185 | //if ( abs( t - last_vedirect) >= VE_WAIT_TIME) { 186 | if ((millis() - last_vedirect_millis) >= VE_WAIT_TIME_MS) { 187 | if ( ve.getNewestBlock(&block)) { 188 | //last_vedirect = t; 189 | last_vedirect_millis = millis(); 190 | log_i("New block arrived; Value count: %d, serial %d", block.kvCount, block.serial); 191 | UpdateCanBusData(&block); 192 | // The send CAN Bus data is handled in a task every second. 193 | //Inverter.SendAllUpdates(); 194 | if ( ((millis() - SendCanBusMQTTUpdates) > 15000) || Inverter.DataChanged() ) 195 | { 196 | log_i("Sending Switch Update Data"); 197 | SendCanBusMQTTUpdates = millis(); 198 | sendUpdateMQTTData(); 199 | } 200 | 201 | if (client.isMqttConnected()) { 202 | sendASCII2MQTT(&block); 203 | } 204 | } 205 | // If CAN Bus has failed to send to many packets we reboot 206 | if (Inverter.CanBusFailed()){ 207 | log_e("Can Bus has too many failed sending events, rebooting."); 208 | delay(50); 209 | ESP.restart(); 210 | } 211 | 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/VEDirect.h: -------------------------------------------------------------------------------- 1 | /* 2 | VE.Direct Protocol. 3 | 4 | GITHUB Link 5 | 6 | MIT License 7 | 8 | Copyright (c) 2020 Ralf Lehmann 9 | Copyright (c) 2022 Simon Jones 10 | 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | */ 30 | 31 | #ifndef VEDIRECT_H 32 | #define VEDIRECT_H 33 | 34 | #include 35 | #include "vedirectSerial.h" 36 | 37 | typedef struct VEDirectKeyValue_t { 38 | String key; 39 | String value; 40 | } _VEDirectKeyValue; 41 | 42 | typedef struct VEDirectBlock_t { 43 | VEDirectKeyValue_t b[MAX_KEY_VALUE_COUNT]; 44 | int kvCount; 45 | int serial; 46 | } _VEDirectBlock; 47 | 48 | enum VEDirectCommands{ 49 | VEDirectSOC = 0x0FFF, 50 | VEDirectV = 0xED8D, 51 | VEDirectC = 0xED8F, 52 | VEDirectUsedAh = 0xEEFF, 53 | VEDirectTemp = 0xEDEC, 54 | VEDirectSyncState = 0xEEB6 55 | } _VEDirectCommands; 56 | 57 | class VEDirect { 58 | public: 59 | 60 | void begin(); 61 | boolean addToASCIIBlock(String s); 62 | boolean getNewestBlock(VEDirectBlock_t *b); 63 | void sendCommand(VEDirectCommands); 64 | 65 | private: 66 | 67 | int _serial = 0; // Serial number of the block 68 | boolean _endOfASCIIBlock(StringSplitter *s); 69 | void _increaseNewestBlock(); 70 | int _getNewestBlock(); 71 | VEDirectBlock_t block[MAX_BLOCK_COUNT]; 72 | portMUX_TYPE _new_block_mutex = portMUX_INITIALIZER_UNLOCKED; // mutex to protect _newest_block 73 | volatile int _newest_block = -1; // newest complete block; ready for consumption 74 | volatile boolean _has_new_block = false; 75 | int _incoming_block = 0; // block currently filled; candidate for next newest block 76 | int _incoming_keyValueCount = 0; 77 | TaskHandle_t tHandle = NULL; 78 | 79 | }; 80 | 81 | /* 82 | This task will collect the data from Serial1 and place them in a block buffer 83 | The task is independant from the main task so it should not loose data because 84 | of MQTT reconnects or other time consuming duties in the main task 85 | */ 86 | void serialTask(void * pointer) { 87 | VEDirect *ve = (VEDirect *) pointer; 88 | String data; 89 | startVEDirectSerial(); 90 | while ( true ) { 91 | if ( Serial1.available()) { 92 | // char s = Serial1.read(); 93 | // log_d("Read: %c:%d", s, s); 94 | // if ( s == '\n') { 95 | // log_d("received start"); 96 | // begin of a datafield or frame 97 | data = Serial1.readStringUntil('\n'); // read label and value 98 | //log_d("Received Data: \"%s\"", data.c_str()); 99 | if ( data.length() > 0) { 100 | data.replace("\r\n", ""); // Strip carriage return newline; not part of the data 101 | ve->addToASCIIBlock(data); 102 | log_d("Stack free: %5d", uxTaskGetStackHighWaterMark(NULL)); 103 | } 104 | //} 105 | } else { 106 | // no serial data available; have a nap 107 | delay(1); 108 | } 109 | } 110 | } 111 | 112 | 113 | void VEDirect::begin() { 114 | _newest_block = -1; 115 | _incoming_block = _incoming_keyValueCount = 0; 116 | // create a task to handle the serial input 117 | xTaskCreate( 118 | &serialTask, /* Task function. */ 119 | "serialTask", /* String with name of task. */ 120 | 10000, /* Stack size in bytes. */ 121 | this, /* Parameter passed as input of the task */ 122 | 1, /* Priority of the task. */ 123 | &tHandle); /* Task handle. */ 124 | } 125 | 126 | void VEDirect::_increaseNewestBlock() { 127 | taskENTER_CRITICAL(&_new_block_mutex); 128 | _newest_block++; 129 | _newest_block %= MAX_BLOCK_COUNT; 130 | taskEXIT_CRITICAL(&_new_block_mutex); 131 | _has_new_block = true; 132 | } 133 | 134 | int VEDirect::_getNewestBlock() { 135 | int n; 136 | taskENTER_CRITICAL(&_new_block_mutex); 137 | n = _newest_block; 138 | taskEXIT_CRITICAL(&_new_block_mutex); 139 | return n; 140 | } 141 | 142 | boolean VEDirect::_endOfASCIIBlock(StringSplitter *s) { 143 | if ( s->getItemAtIndex(0).equals("Checksum")) { 144 | // To Do: checksum test 145 | 146 | return true; 147 | } 148 | return false; 149 | } 150 | 151 | boolean VEDirect::addToASCIIBlock(String s) { 152 | StringSplitter sp = StringSplitter(s, '\t', 2); 153 | String historical = ""; 154 | if ( sp.getItemCount() == 2) { // sometime checksum has historical data attached 155 | log_v("Received Key/value: \"%s\":\"%s\"", sp.getItemAtIndex(0).c_str(), sp.getItemAtIndex(1).c_str()); 156 | if ( _incoming_keyValueCount < MAX_KEY_VALUE_COUNT) { 157 | block[_incoming_block].b[_incoming_keyValueCount].key = sp.getItemAtIndex(0); 158 | if ( sp.getItemAtIndex(0).equals("Checksum")) { 159 | char s[10]; 160 | sprintf(s, "%02x", sp.getItemAtIndex(1).charAt(0)); 161 | block[_incoming_block].b[_incoming_keyValueCount].value = String(s); 162 | historical = sp.getItemAtIndex(1).substring(1); 163 | } else { 164 | block[_incoming_block].b[_incoming_keyValueCount].value = sp.getItemAtIndex(1); 165 | } 166 | block[_incoming_block].kvCount = ++_incoming_keyValueCount; 167 | if ( historical.length() > 1) { 168 | // historical data 169 | block[_incoming_block].b[_incoming_keyValueCount].key = "Historical"; 170 | block[_incoming_block].b[_incoming_keyValueCount].value = historical; 171 | log_v("Historical data:\"%s\"", historical.c_str()); 172 | block[_incoming_block].kvCount = ++_incoming_keyValueCount; 173 | } 174 | } else { 175 | // buffer full but not end of frame 176 | // so this is not a good frame -> delete frame 177 | _incoming_keyValueCount = 0; 178 | return false; // buffer full 179 | } 180 | if ( _endOfASCIIBlock(&sp)) { 181 | // good frame; increase newest frame pointer 182 | block[_incoming_block].serial = _serial++; 183 | _increaseNewestBlock(); 184 | _incoming_keyValueCount = 0; 185 | _incoming_block++; 186 | _incoming_block %= MAX_BLOCK_COUNT; 187 | return true; 188 | } 189 | // not the end of a frame yet; continue 190 | return false; 191 | } else { 192 | log_e("Received Data not correct: \"%s\"", s.c_str()); 193 | //delete splitter; 194 | return false; 195 | } 196 | } 197 | 198 | boolean VEDirect::getNewestBlock(VEDirectBlock_t *b) { 199 | if ( ! _has_new_block) { 200 | return false; 201 | } 202 | _has_new_block = false; 203 | int block_num = _getNewestBlock(); 204 | int kv_count = block[block_num].kvCount; 205 | b->kvCount = kv_count; 206 | int ser = block[block_num].serial; 207 | b->serial = ser; 208 | for (int i = 0; i < kv_count; i++) { 209 | b->b[i] = block[block_num].b[i]; 210 | } 211 | return true; 212 | } 213 | 214 | 215 | 216 | #endif 217 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | /* 2 | VE.Direct config file. 3 | Cconfiguration parameters for 4 | VE.Direct2MQTT gateway 5 | 6 | GITHUB Link 7 | 8 | MIT License 9 | 10 | Copyright (c) 2020 Ralf Lehmann 11 | Copyright (c) 2022 Simon Jones 12 | 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all 22 | copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | */ 32 | 33 | /* 34 | Defines that activate features like OTA (over the air update) 35 | or Acitve/Passive mode 36 | */ 37 | 38 | //#define CAN_INT 22 // CAN Init Pin for M5Stack 39 | #define CAN_CS_PIN 2 // CAN CS PIN 40 | #define initBattChargeVoltage 56000 // Battery Charge Voltage sent to inverter 41 | #define initBattDischargeVoltage 45000 // Battery discharge voltage, not currently used 42 | #define initBattChargeCurrent 100000 // in mA, this is just the initial / max setting, it will self adjust 43 | #define initBattDischargeCurrent 100000 // in mA 44 | #define initBattCapacity 475000 // used for charge limits when batteries becoming full. 45 | 46 | // To use PYLONTECH Protocol enable below 47 | //#define USE_PYLONTECH 48 | 49 | // use SSL to connect to MQTT Server or OTA Server 50 | // it is strongly recommended to use SSL if you send any password over the net 51 | // connectiong to MQTT might need a password; the same for OTA 52 | // if you do not have SSL activated on your servers rename it to NO_USE_SSL 53 | #define NO_USE_SSL 54 | 55 | // Activate Over The Air Update of firmware 56 | #define USE_OTA 57 | 58 | // 59 | // Use OneWire temperature sensors 60 | // 61 | //#define USE_ONEWIRE 62 | 63 | #ifdef USE_ONEWIRE 64 | #define ONEWIRE_PIN 22 65 | /* 66 | define the wait time between 2 attempts to send one wire data 67 | 300000 = every 5 minutes 68 | */ 69 | int OW_WAIT_TIME = 10; // in s 70 | time_t last_ow; 71 | #endif 72 | 73 | 74 | 75 | #ifdef USE_SSL 76 | /* 77 | SSL certificate 78 | This is good for all let's encrypt certificates for MQTT or OTA servers 79 | */ 80 | /* 81 | This is lets-encrypt-x3-cross-signed.pem 82 | */ 83 | const char* rootCACertificate = \ 84 | "-----BEGIN CERTIFICATE-----\n" \ 85 | "MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/\n" \ 86 | "MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\n" \ 87 | "DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow\n" \ 88 | "SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT\n" \ 89 | "GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC\n" \ 90 | "AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF\n" \ 91 | "q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8\n" \ 92 | "SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0\n" \ 93 | "Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA\n" \ 94 | "a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj\n" \ 95 | "/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T\n" \ 96 | "AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG\n" \ 97 | "CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv\n" \ 98 | "bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k\n" \ 99 | "c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw\n" \ 100 | "VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC\n" \ 101 | "ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz\n" \ 102 | "MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu\n" \ 103 | "Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF\n" \ 104 | "AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo\n" \ 105 | "uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/\n" \ 106 | "wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu\n" \ 107 | "X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG\n" \ 108 | "PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6\n" \ 109 | "KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==\n" \ 110 | "-----END CERTIFICATE-----\n"; 111 | 112 | #endif 113 | 114 | 115 | /* 116 | WiFi parameters 117 | */ 118 | 119 | // WiFi SSID'S and passwords 120 | // the strongest WiFi station will be used 121 | const char* ssid = ""; 122 | const char* pw = ""; 123 | 124 | /* 125 | MQTT parameters 126 | ATTENTION: use a unique client id to connect to MQTT or you will be kicked out by another device 127 | using your id 128 | */ 129 | //#define MQTT_MAX_RETRIES 3 // maximum retires to reach a MQTT broker 130 | const char* mqtt_server = ""; 131 | // no SSL ports 132 | const uint16_t mqtt_port = 1883; 133 | // SSL ports 134 | //const uint16_t mqtt_port[] = {8883, 8883}; 135 | const char* mqtt_clientID = "vedirectmqtt"; 136 | const char* mqtt_username = "emon"; 137 | const char* mqtt_password = "emonmqtt"; 138 | //int mqtt_server_count = sizeof(mqtt_server) / sizeof(mqtt_server[0]); 139 | 140 | // this is the MQTT prefix; below that we use the string from VE.Direct 141 | // e.g. /MPPT75-15/PID for Product ID 142 | String MQTT_PREFIX = "SMARTBMS"; 143 | String MQTT_PARAMETER = "/Parameter"; 144 | #ifdef USE_ONEWIRE 145 | String MQTT_ONEWIRE = "/Temp/OneWire"; 146 | #endif 147 | 148 | 149 | 150 | /* 151 | Software serial parameter 152 | These are the pins for the VE.Direct connection 153 | WARNING: if your VE.Direct device uses 5V please use a 1kOhm/2kOhm divider for the receive line 154 | The sending line does not need any modification. The ESP uses 3.3V and that's it. A 5V device 155 | should be able to read that voltage as input 156 | */ 157 | #ifndef VEDIRECT_RX 158 | #define VEDIRECT_RX 33 // connected to TX of the VE.Direct device; ATTENTION divider may be needed, see abowe 159 | #endif 160 | #ifndef VEDIRECT_TX 161 | #define VEDIRECT_TX 32 // connected to RX of the VE:DIRECT device 162 | #endif 163 | 164 | /* 165 | Depending on the DE.Direct device there will be several Key/Value pairs; 166 | Define the maximum count of key/value pairs 167 | */ 168 | #define MAX_KEY_VALUE_COUNT 30 169 | 170 | /* 171 | Number of Key-Value blocks we can buffer 172 | MQTT may be slower than one second, especially when we have to reconnect 173 | this is the number of buffers we can keep 174 | */ 175 | #define MAX_BLOCK_COUNT 5 176 | 177 | /* 178 | Wait time in Loop 179 | this determines how many frames are send to MQTT 180 | if wait time is e.g. 10 minutes, we will send only every 10 minutes to MQTT 181 | Note: only the last incoming block will be send; all previous blocks will be discarded 182 | Wait time is in seconds 183 | Waittime of 1 or 0 means every received packet will be transmitted to MQTT 184 | Packets during OTA or OneWire will be discarded 185 | */ 186 | int VE_WAIT_TIME = 1; // in s 187 | uint16_t VE_WAIT_TIME_MS = 10; 188 | -------------------------------------------------------------------------------- /src/CANBUS.cpp: -------------------------------------------------------------------------------- 1 | #include "CANBUS.h" 2 | 3 | void canSendTask(void * pointer){ 4 | CANBUS *Inverter = (CANBUS *) pointer; 5 | log_i("Starting CAN Bus send task"); 6 | for (;;) { 7 | if(Inverter->SendAllUpdates()) 8 | log_d("Success from SendAllUpdates"); 9 | else 10 | log_e("Failure returned from SendAllUpdates"); 11 | vTaskDelay(1000 / portTICK_PERIOD_MS); 12 | } 13 | 14 | } 15 | 16 | bool CANBUS::Begin(uint8_t _CS_PIN) { 17 | 18 | if (CAN != NULL) 19 | { 20 | delete(CAN); 21 | } 22 | 23 | CAN = new MCP_CAN(_CS_PIN); 24 | 25 | log_i("CAN Bus Initialising"); 26 | // Initialize MCP2515 running at 8MHz with a baudrate of 500kb/s and the masks and filters disabled. 27 | if (CAN->begin(MCP_ANY, CAN_500KBPS, MCP_8MHZ) == CAN_OK) 28 | { 29 | // Change to normal mode to allow messages to be transmitted 30 | CAN->setMode(MCP_NORMAL); 31 | log_i("CAN Bus Initialised"); 32 | _initialised = true; 33 | LoopTimer = millis(); 34 | } 35 | else 36 | { 37 | log_e("CAN Bus Failed to Initialise"); 38 | _initialised = false; 39 | return false; 40 | } 41 | 42 | // Create a task to send the CAN Bus data so Wifi/MQTT doesn't 43 | // interfere with it. 44 | xTaskCreate( 45 | &canSendTask, 46 | "canSendTask", 47 | 10000, 48 | this, 49 | 2, 50 | &tHandle); 51 | 52 | // Get the Pylontech protocol setting from EEPROM if set. 53 | if(pref.isKey(ccPylonTech)) 54 | _enablePYLONTECH = pref.getBool(ccPylonTech, _enablePYLONTECH); 55 | return true; 56 | } 57 | 58 | bool CANBUS::SendAllUpdates() 59 | { 60 | log_i("Sending all CAN Bus Data"); 61 | // Turn off force charge, this is defined in PylonTech Protocol 62 | if (_battSOC > 96 && _forceCharge){ 63 | ForceCharge(false); 64 | } 65 | 66 | if (_battCapacity > 0 && _initialBattData){ 67 | if(_battSOC > 95) 68 | _chargeCurrentmA = (_battCapacity / 20); 69 | else if(_battSOC > 90) 70 | _chargeCurrentmA = (_battCapacity / 10); 71 | else 72 | _chargeCurrentmA = _maxChargeCurrentmA; 73 | } 74 | 75 | if(!_initialBattCurrent) 76 | log_i("Waiting on VE Initial Battery Current."); 77 | if(!_initialBattVoltage) 78 | log_i("Waiting on VE Initial Battery Voltage."); 79 | if(!_initialBattSOC) 80 | log_i("Waiting on VE Initial Battery SOC."); 81 | if(!_initialChargeCurrent) 82 | log_e("Initial Charge Current needs to be set."); 83 | if(!_initialChargeVoltage) 84 | log_e("Initial Charge Voltage needs to be set."); 85 | if(!_initialDischargeCurrent) 86 | log_e("Initial Discharge Current needs to be set."); 87 | if(!_initialDischargeVoltage) 88 | log_e("Initial Discharge Voltage needs to be set."); 89 | 90 | //if (Initialised() && AllReady() && ((millis() - LoopTimer) > _CanBusSendInterval)) 91 | if (Initialised() && Configured()) 92 | { 93 | if (SendParamUpdate() && SendBattUpdate()) { 94 | //LoopTimer = millis(); 95 | return true; 96 | } else return false; 97 | } 98 | else 99 | { 100 | log_e("CAN Bus Data not Initialised or Configured"); 101 | return false; 102 | } 103 | 104 | } 105 | 106 | bool CANBUS::SendBattUpdate() 107 | { 108 | return SendBattUpdate(_battSOC,_battVoltage,_battCurrentmA, _battTemp, _battSOH); 109 | } 110 | 111 | bool CANBUS::SendBattUpdate(uint8_t SOC, uint16_t Voltage, int32_t CurrentmA, int16_t BattTemp, uint8_t SOH = 100) 112 | { 113 | byte sndStat; 114 | // Send SOC and SOH first 115 | if (!Initialised() && !Configured()) return false; 116 | 117 | if(_enablePYLONTECH) { 118 | CAN_MSG[0] = lowByte(SOC); 119 | CAN_MSG[1] = highByte(SOC); 120 | } else if (_forceCharge) { 121 | CAN_MSG[0] = lowByte((int8_t) 1); 122 | CAN_MSG[1] = highByte((int8_t) 1); 123 | } else { 124 | CAN_MSG[0] = lowByte(SOC); 125 | CAN_MSG[1] = highByte(SOC); 126 | } 127 | 128 | CAN_MSG[2] = lowByte(SOH); 129 | CAN_MSG[3] = highByte(SOH); 130 | CAN_MSG[4] = 0; 131 | CAN_MSG[5] = 0; 132 | CAN_MSG[6] = 0; 133 | CAN_MSG[7] = 0; 134 | 135 | sndStat = CAN->sendMsgBuf(0x355, 0, 4, CAN_MSG); 136 | if(sndStat == CAN_OK){ 137 | _failedCanSendCount = 0; 138 | log_i("Inverter SOC Battery update via CAN Bus sent."); 139 | } else { 140 | _failedCanSendCount++; 141 | log_e("Inverter SOC Battery update via CAN Bus failed."); 142 | } 143 | delay(_canSendDelay); 144 | 145 | // Current measured values of the BMS battery voltage, battery current, battery temperature 146 | 147 | CAN_MSG[0] = lowByte(uint16_t(Voltage)); 148 | CAN_MSG[1] = highByte(uint16_t(Voltage)); 149 | CAN_MSG[2] = lowByte(uint16_t(CurrentmA)); 150 | CAN_MSG[3] = highByte(uint16_t(CurrentmA)); 151 | CAN_MSG[4] = lowByte(uint16_t(BattTemp * 10)); 152 | CAN_MSG[5] = highByte(uint16_t(BattTemp * 10)); 153 | CAN_MSG[6] = 0x00; 154 | CAN_MSG[7] = 0x00; 155 | 156 | sndStat = CAN->sendMsgBuf(0x356, 0, 8, CAN_MSG); 157 | 158 | if(sndStat == CAN_OK){ 159 | _failedCanSendCount = 0; 160 | log_i("Inverter Battery Voltage, Current update via CAN Bus sent."); 161 | } else { 162 | _failedCanSendCount++; 163 | log_e("Inverter Battery Voltage, Current update via CAN Bus failed."); 164 | } 165 | 166 | delay(_canSendDelay); 167 | 168 | //if (_enablePYLONTECH){ 169 | //0x359 – 00 00 00 00 0A 50 4E – Protection & Alarm flags 170 | CAN_MSG[0] = 0x00; 171 | CAN_MSG[1] = 0x00; 172 | CAN_MSG[2] = 0x00; 173 | CAN_MSG[3] = 0x00; 174 | CAN_MSG[4] = 0x0A; 175 | CAN_MSG[5] = 0x50; 176 | CAN_MSG[6] = 0x4E; 177 | CAN_MSG[7] = 0x00; 178 | 179 | sndStat = CAN->sendMsgBuf(0x359, 0, 8, CAN_MSG); 180 | if(sndStat == CAN_OK){ 181 | _failedCanSendCount = 0; 182 | log_i("Inverter Protection / Alarm Flags via CAN Bus sent."); 183 | } else { 184 | _failedCanSendCount++; 185 | log_e("Inverter Protection / Alarm Flags via CAN Bus failed."); 186 | } 187 | delay(_canSendDelay); 188 | 189 | //0x35C – C0 00 – Battery charge request flags 190 | CAN_MSG[0] = 0xC0; 191 | CAN_MSG[1] = 0x00; 192 | if (_forceCharge) CAN_MSG[1] | bmsForceCharge; 193 | if (_chargeEnabled) CAN_MSG[1] | bmsChargeEnable; 194 | if (_dischargeEnabled) CAN_MSG[1] | bmsDischargeEnable; 195 | CAN_MSG[2] = 0x00; 196 | CAN_MSG[3] = 0x00; 197 | CAN_MSG[4] = 0x00; 198 | CAN_MSG[5] = 0x00; 199 | CAN_MSG[6] = 0x00; 200 | CAN_MSG[7] = 0x00; 201 | 202 | sndStat = CAN->sendMsgBuf(0x35C, 0, 2, CAN_MSG); 203 | if(sndStat == CAN_OK){ 204 | _failedCanSendCount = 0; 205 | log_i("Battery Charge Flags via CAN Bus sent."); 206 | } else { 207 | _failedCanSendCount++; 208 | log_e("Battery Charge Flags via CAN Bus failed."); 209 | } 210 | delay(_canSendDelay); 211 | //} 212 | return true; 213 | } 214 | 215 | 216 | void CANBUS::SetChargeVoltage(uint32_t Voltage){ 217 | 218 | if(!_initialChargeVoltage) 219 | { 220 | _initialChargeVoltage = true; 221 | if(!pref.isKey(ccChargeVolt)) 222 | pref.putUInt(ccChargeVolt,Voltage); 223 | else 224 | Voltage = pref.getUInt(ccChargeVolt,Voltage); 225 | } 226 | 227 | if(_chargeVoltage != Voltage) { 228 | _dataChanged = true; 229 | _chargeVoltage = Voltage; 230 | pref.putUInt(ccChargeVolt,Voltage); 231 | } 232 | 233 | } 234 | 235 | void CANBUS::SetChargeCurrent(uint32_t CurrentmA){ 236 | if(!_initialChargeCurrent) { 237 | if(!pref.isKey(ccChargeCurrent)) { 238 | _maxChargeCurrentmA = CurrentmA; 239 | pref.putUInt(ccChargeCurrent,CurrentmA); 240 | } else 241 | _maxChargeCurrentmA = pref.getUInt(ccChargeCurrent,CurrentmA); 242 | _initialChargeCurrent = true; 243 | } 244 | else if (_chargeCurrentmA != CurrentmA && _initialDone) { 245 | _dataChanged = true; 246 | _chargeCurrentmA = CurrentmA; 247 | } else 248 | return; 249 | } 250 | 251 | void CANBUS::SetDischargeVoltage(uint32_t Voltage){ 252 | _initialDischargeVoltage = true; 253 | _dischargeVoltage = Voltage; 254 | } 255 | 256 | void CANBUS::SetDischargeCurrent(uint32_t CurrentmA){ 257 | if(!_initialDischargeCurrent) { 258 | _maxDischargeCurrentmA = CurrentmA; 259 | _initialDischargeCurrent = true; 260 | } 261 | if (_dischargeCurrentmA != CurrentmA && _initialDone) { 262 | _dischargeCurrentmA = CurrentmA; 263 | _dataChanged = true; 264 | } 265 | } 266 | 267 | void CANBUS::ForceCharge(bool State) { 268 | if (State != _forceCharge) _dataChanged = true; 269 | _forceCharge = State; 270 | } 271 | 272 | void CANBUS::ChargeEnable(bool State) { 273 | if (State != _chargeEnabled) _dataChanged = true; 274 | _chargeEnabled = State; 275 | } 276 | 277 | void CANBUS::DischargeEnable(bool State) { 278 | if (State != _dischargeEnabled) _dataChanged = true; 279 | _dischargeEnabled = State; 280 | } 281 | void CANBUS::EnablePylonTech(bool enable){ 282 | pref.putBool(ccPylonTech,enable); 283 | _enablePYLONTECH = enable; 284 | } 285 | 286 | bool CANBUS::DataChanged(){ 287 | if (_dataChanged) { 288 | _dataChanged = false; 289 | return true; 290 | } else return false; 291 | } 292 | 293 | 294 | bool CANBUS::SendParamUpdate(){ 295 | 296 | byte sndStat; 297 | 298 | if (!Initialised() && !Configured()) return false; 299 | 300 | // Send PYLON String if enabled 301 | //if(_enablePYLONTECH) { 302 | sndStat = CAN->sendMsgBuf(0x35E, 0, 8, MSG_PYLON); 303 | if (sndStat == CAN_OK){ 304 | log_i("Sent PYLONTECH String."); 305 | } else 306 | log_i("Failed to send PYLONTECH String."); 307 | delay(_canSendDelay); 308 | //} 309 | 310 | // Battery charge and discharge parameters 311 | CAN_MSG[0] = lowByte(_chargeVoltage / 100); // Maximum battery voltage 312 | CAN_MSG[1] = highByte(_chargeVoltage / 100); 313 | if(_chargeEnabled || _enablePYLONTECH){ 314 | CAN_MSG[2] = lowByte(_chargeCurrentmA / 100); // Maximum charging current 315 | CAN_MSG[3] = highByte(_chargeCurrentmA / 100); 316 | } else { 317 | CAN_MSG[2] = lowByte(0); // Maximum charging current 318 | CAN_MSG[3] = highByte(0); 319 | } 320 | if(_dischargeEnabled || _enablePYLONTECH){ 321 | CAN_MSG[4] = lowByte(_dischargeCurrentmA / 100); // Maximum discharge current 322 | CAN_MSG[5] = highByte(_dischargeCurrentmA / 100); 323 | } else { 324 | CAN_MSG[4] = lowByte(0); // Maximum discharge current 325 | CAN_MSG[5] = highByte(0); 326 | } 327 | CAN_MSG[6] = lowByte(_dischargeVoltage / 100); // Currently not used by SOLIS 328 | CAN_MSG[7] = highByte(_dischargeVoltage / 100); // Currently not used by SOLIS 329 | 330 | sndStat = CAN->sendMsgBuf(0x351, 0, 8, CAN_MSG); 331 | 332 | if(sndStat == CAN_OK){ 333 | log_i("Inverter Parameters update via CAN Bus sent."); 334 | } else 335 | { 336 | log_i("Inverter Parameters update via CAN Bus failed."); 337 | } 338 | 339 | return true; 340 | 341 | } 342 | 343 | bool CANBUS::AllReady() 344 | { 345 | if (_initialDone) return true; 346 | else if (_initialBattSOC && _initialBattVoltage && _initialBattCurrent && 347 | _initialChargeVoltage && _initialChargeCurrent && _initialDischargeVoltage && _initialDischargeCurrent) 348 | { 349 | _dischargeCurrentmA = _maxDischargeCurrentmA; 350 | _chargeCurrentmA = _maxChargeCurrentmA; 351 | _initialDone = true; 352 | _initialConfig = true; 353 | _initialBattData = true; 354 | return true; 355 | } 356 | else if (_initialChargeVoltage && _initialChargeCurrent 357 | && _initialDischargeVoltage && _initialDischargeCurrent &&(!_initialConfig)) 358 | { 359 | _initialConfig = true; 360 | return false; 361 | } 362 | else 363 | return false; 364 | } 365 | 366 | bool CANBUS::Configured() 367 | { 368 | AllReady(); // Check if we need to set the flags 369 | 370 | if (_initialConfig) return true; 371 | else if (_initialChargeVoltage && _initialChargeCurrent 372 | && _initialDischargeVoltage && _initialDischargeCurrent) 373 | { 374 | _initialConfig = true; 375 | return true; 376 | } 377 | else return false; 378 | } --------------------------------------------------------------------------------