Chemcool Electronics

Chemcool Electronics Blog

Read Victron EasySolar Controller Advertised Data

 

References:

 

https://github.com/chrisj7903/Read-Victron-advertised-data/blob/main/SolarController/SolarController.ino

 

I used the given Arduino code as above and adjusted the code for a Heltec WiFi Kit V3 board with an OLED display.

 

The code below display the received Victron EasySolar Charger Bluetooth advertised data on te OLED screen.

 

In addition, I adjusted the code to communicate the variables to my Thingspeek channel. You can see the picture accordingly.

 

  • Therefore, now I can monitor my Victron EasySolar charge controller worldwide and not restricted to the short BLE signal distance. 
  • Then I added a digital output pin to control a relay which in my case will trigger a Sonoff Mini D, which access my Sonoff network to controll the solar battery charger on or off from the grid supply.
  • I also activted the build in LED for status indications.

 

Here is the code, BUT TAKE NOTE THERE ARE ALSO ADDED FILES VCC, and ZZ you need to adjust with your own secret keys and MAC address (see the reference link above):

 

#define HELTEC_BOARD true

#define SLOW_CLK_TYPE 1

#include "ZZ.h"

#include "VSC.h"  // Victron Solar Controller

#include <user_settings.h>

#include <wolfssl.h>

#include <WiFi.h>

#include <HTTPClient.h>

#include <Wire.h>

#include <heltec.h>

#include "HT_SSD1306Wire.h"

 

#define OLED_ADDR 0x3C

 

// OLED setup for Heltec WiFi Kit V3 (SSD1306 128x64)

SSD1306Wire display(OLED_ADDR, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED);

 

// WiFi credentials

const char* ssid = "..........";  // WiFi SSID

const char* password = "..............";  // WiFi password

 

// Thingspeak API details

const char* thingspeakServer = "api.thingspeak.com";

const String apiKey = ".................";  // Your Thingspeak API write key

const String channelID = ".............";  // Your Thingspeak channel ID

 

// Digital output pin for voltage control

const int outputPin = 46;  // GPIO 46, safe for Heltec V3 (not used by OLED or Vext)

const int ledPin = 37;  // Built-in LED, GPIO 37 on Heltec V3 (GPIO 35 suggested, verify if needed)

 

int delay_ms = 500;

unsigned long lastThingspeakUpdate = 0;

const unsigned long thingspeakInterval = 60000;  // Update every 60 seconds

 

// Function to control external power (Vext)

void VextON() {

  pinMode(21, OUTPUT);

  digitalWrite(21, LOW);

}

 

void setup() {

  Serial.begin(115200);

  while (!Serial && millis() < 2000) ;  // wait for serial, up to 1 sec

  Serial << "\n\n======== Solar Controller ========\n";  

  Serial << "* wolfssl : V" << LIBWOLFSSL_VERSION_STRING << '\n';  

  Serial << "* Target  : " << VICTRON_NAME << '\n';

  Serial << "* Enter V to toggle VERBOSE mode ON/OFF\n";

 

  // Initialize digital output pin

  pinMode(outputPin, OUTPUT);

  digitalWrite(outputPin, HIGH);  // Start with pin HIGH

  Serial << "* Digital output pin initialized: GPIO " << outputPin << '\n';

 

  // Initialize built-in LED

  pinMode(ledPin, OUTPUT);

  digitalWrite(ledPin, LOW);  // Start with LED OFF (outputPin is HIGH)

  Serial << "* Built-in LED initialized: GPIO " << ledPin << '\n';

 

  // Initialize OLED

  VextON();  // Power on OLED

  display.init();

  display.clear();

  display.setFont(ArialMT_Plain_10);  // 10-pixel font

  display.drawString(0, 0, "Initializing...");

  display.display();

  Serial << "* OLED initialized\n";

 

  // Initialize WiFi

  Serial << "* Connecting to WiFi: " << ssid << '\n';

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {

    delay(500);

    Serial << ".";

  }

  Serial << "\n* WiFi connected, IP address: " << WiFi.localIP() << '\n';

 

  // Initialize BLE

  Serial << "* init BLE ...\n";

  BLEDevice::init("");

  Serial << "* setup scan ...\n";

  pBLEScan->setAdvertisedDeviceCallbacks(new AdDataCallback());

  pBLEScan->setActiveScan(true);  // uses more power, but get results faster

  //pBLEScan->setInterval(100);   // time between consecutive scan starts

  //pBLEScan->setWindow(99);      // duration of each scan, less or equal setInterval value

  Serial << "* scan for devices every " << delay_ms << " ms (and up to " << scan_secs << " secs/scan)\n";

  Serial << CF(dashes) << "setup done" << CF(dashes) << '\n';

  displayHeadings();

}

 

uint32_t loopCount = 0;

 

void loop() {

  loopCount++;

  processSerialCommands();

  pBLEScan->start(scan_secs, false);

  delay(delay_ms);

  if (VERBOSE) Serial << '\n';

  printLoopCount();

  if (VERBOSE) {

    Serial << CF(line);

    Serial << "data  : "; printBIGarray(); Serial << '\n';

  }

  decryptAesCtr(VERBOSE);

  if (VERBOSE) {

    Serial << "output: "; printByteArray(output); Serial << '\n';

    //Serial << "binary:\n"; printBins(output); Serial << '\n';

    Serial << "values: ";

  }

  reportSCvalues();

 

  // Control digital output and LED

  controlDigitalOutput();

 

  // Update OLED display

  updateOledDisplay();

 

  // Send data to Thingspeak every 60 seconds

  if (millis() - lastThingspeakUpdate >= thingspeakInterval) {

    sendToThingspeak();

    lastThingspeakUpdate = millis();

  }

} // loop

 

void displayHeadings() {

  Serial << '\n';

  Serial << "\tstate " << "error  " << "battV " << "battA  " << "  kWh   " << "Watts " << "loadA\n";  

  Serial << "\t----- " << "-----  " << "----- " << "-----  " << "------- " << "----- " << "-----\n";  

}

 

void printLoopCount() {

  if (loopCount < 100) Serial << "0";

  if (loopCount < 10) Serial << "0";

  Serial << loopCount << ":\t";  

}

 

void controlDigitalOutput() {

  static bool pinState = false;  // Track current state (false = HIGH, true = LOW)

  float battV = parseBattVolts();

 

  if (!na_batV) {  // Only update if voltage is valid

    if (battV < 12.3 && !pinState) {

      digitalWrite(outputPin, LOW);

      pinState = true;

      digitalWrite(ledPin, HIGH);  // LED ON when outputPin is LOW

      Serial << "* GPIO " << outputPin << " set LOW (battV = " << _FLOAT(battV, 2) << "V < 12.3V), LED ON\n";

    } else if (battV > 13.3 && pinState) {

      digitalWrite(outputPin, HIGH);

      pinState = false;

      digitalWrite(ledPin, LOW);  // LED OFF when outputPin is HIGH

      Serial << "* GPIO " << outputPin << " set HIGH (battV = " << _FLOAT(battV, 2) << "V > 13.3V), LED OFF\n";

    }

  }

}

 

void updateOledDisplay() {

  // Get the values to display (same as sent to Thingspeak)

  String status = getDeviceStateString();

  float battV = parseBattVolts();

  float battA = parseBattAmps();

  float kWh = parseKWHtoday();

  float pvW = parsePVpower();

 

  // Prepare strings, handle NaN cases

  String voltStr = na_batV ? "NaN" : String(battV, 2) + "V";

  String ampStr = na_batA ? "NaN" : String(battA, 1) + "A";

  String kwhStr = na_kWh ? "NaN" : String(kWh, 2) + "kWh";

  String wattStr = na_pvW ? "NaN" : String(pvW, 0) + "W";

  String statusStr = (status == "1") ? "BULK" : "OFF";  // Match Thingspeak digital values

 

  // Update OLED

  display.clear();

  display.setFont(ArialMT_Plain_10);  // 10-pixel font

  display.drawString(0, 0, "Status: " + statusStr);

  display.drawString(0, 10, "Volts: " + voltStr);

  display.drawString(0, 20, "Amps: " + ampStr);

  display.drawString(0, 30, "kWh: " + kwhStr);

  display.drawString(0, 40, "Watts: " + wattStr);

  display.display();

}

 

void sendToThingspeak() {

  if (WiFi.status() == WL_CONNECTED) {

    HTTPClient http;

 

    // Get the values to send

    String status = getDeviceStateString();

    float battV = parseBattVolts();

    float battA = parseBattAmps();

    float kWh = parseKWHtoday();

    float pvW = parsePVpower();

    // float loadA = parseLoadAmps();  // Excluded as per channel setup (Status, Volts, Amperes, kWh, Watts)

 

    // Construct the Thingspeak URL

    String url = "https://" + String(thingspeakServer) + "/update?api_key=" + apiKey;

    url += "&field1=" + (na_batV ? "NaN" : String(battV, 2));

    url += "&field2=" + (na_batA ? "NaN" : String(battA, 1));

    url += "&field3=" + (na_kWh ? "NaN" : String(kWh, 2));

    url += "&field4=" + (na_pvW ? "NaN" : String(pvW, 0));

    url += "&field5=" + status;  // Status as field5 to match channel order

 

    // Start the HTTP request

    http.begin(url);

    int httpCode = http.GET();

   

    if (httpCode == HTTP_CODE_OK) {

      Serial << "* Data sent to Thingspeak successfully\n";

    } else {

      Serial << "* Failed to send to Thingspeak, HTTP code: " << httpCode << '\n';

    }

    http.end();

  } else {

    Serial << "* WiFi disconnected, attempting to reconnect...\n";

    WiFi.reconnect();

  }

}

 

String getDeviceStateString() {

  // Convert device state to digital value: 1 for BULK, 2 for ABSORPTION, 3 for FLOAT, 0 for OFF or other states

  switch (output[0]) {

    case 3: return "1";  // BULK

    case 4: return "2";  // ABSORPTION

    case 5: return "3";  // FLOAT

    default: return "0";  // OFF or any other state

  }

}