├── 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 | }
--------------------------------------------------------------------------------