Note
Here I documented some code parts with explanations to be able to better understand and extend the project. Keep in mind that some translations have been performed with ChatGPT. So I hope everything is making sense at the end - Please checkout the real code for a working set.
- SPIDR.ino - Main Program
- Display Manager
- Inverter Class Documentation
- MQTTManager Class Documentation
To understand the main Flow of SPIDR, you shopuld try to understand the following Flowchart. It might not be perfectly in line with the code, but fully explains the general idea.
void setup() {
Serial.begin(115200); // Starts the serial connection
delay(500); // Short pause
prefsManager.begin(); // Initializes the settings manager
loadPreferencesIntoVariables(); // Loads settings into variables
displayManager.init(); // Initializes the display with a start screen
action.name = "S|P|I|D|R"; // Sets the action name
displayManager.displayAction(action); // Displays the action on the display
setupTime(); // Initializes the time
mqttManager = new MQTTManager(MQTT_BROKER_HOST.c_str(), MQTT_BROKER_PORT,
MQTT_BROKER_USER.c_str(), MQTT_BROKER_PWD.c_str(), displayManager,
inverter, serialCapture); // Initializes the MQTT Manager
currentState = HOME_NETWORK_MODE; // Sets the current state
inverter.initializeDataBuffer(0); // Initializes the inverter data buffer
}void loop() {
static unsigned long lastSetupTimeCalled = 0; // Stores when setupTime was last called
const unsigned long intervalSetupTime = 120000; // Interval for setupTime in milliseconds (2 minutes)
static unsigned long lastStatemachineCalled = 0; // Stores when the state machine was last called
const unsigned long intervalStatemachine = 200; // Interval for the state machine in milliseconds
if (millis() - lastStatemachineCalled >= intervalStatemachine) {
updateStateMachine(); // Updates the state machine
lastStatemachineCalled = millis(); // Updates the time of the last call
}
if (millis() - lastSetupTimeCalled >= intervalSetupTime) {
setupTime(); // Updates the time
lastSetupTimeCalled = millis(); // Updates the time of the last call
}
delay(5); // Short delay to slow down the loop
}
void updateStateMachine() {
switch (currentState) {
case INVERTER_NETWORK_MODE: // Handles the inverter network mode
handleInverterNetworkMode();
break;
case HOME_NETWORK_MODE: // Handles the home network mode
handleHomeNetworkMode();
break;
case AP_MODE: // Handles the access point mode
handleAPMode();
break;
}
}void setupTime() {
// Uses NTP time or, if unavailable, the build time
// ...
}
void activateAPMode() {
WiFi.mode(WIFI_AP); // Enables AP+STA mode for simultaneous access point and Wi-Fi client mode
WiFi.softAP(WIFI_AP_NAME, WIFI_AP_PASSWORD);
webServerManager.begin();
}
void deactivateAPMode() {
WiFi.mode(WIFI_STA);
webServerManager.stop();
}bool readInverterDataFromWebInterface(String url, String web_user, String web_password) {
String serverIp = WiFi.gatewayIP().toString(); // Converts the gateway IP to a string
String website = "http://" + serverIp + "/" + url; // Creates the URL for the webpage
bool readHTML = false; // Initializes the variable that indicates whether HTML was read
WiFiClient client; // Creates a WiFiClient object
if (client.connect(serverIp.c_str(), 80)) { // Connects to the server over port 80
// Creates the value for HTTP Basic Authentication
String authHeaderValue = "Basic " + base64::encode(web_user + ":" + web_password);
client.println("GET /" + url + " HTTP/1.1"); // Sends the HTTP GET Request
client.println("Host: " + serverIp); // Sends the Host header
client.println("Authorization: " + authHeaderValue); // Sends the Authorization header
client.println("Connection: close"); // Requests that the connection be closed after the response
client.println(); // Ends the HTTP header
String response = ""; // Initializes the response string
String line = ""; // Initializes the line string
while (client.connected()) { // As long as the client is connected
while (client.available()) { // As long as data from the server is available
char c = client.read(); // Reads the next character
if (c == '\n') { // If the character is a newline
if (line.indexOf("<H4>401 Unauthorized</H4>") > 0) { // Checks for a 401 Unauthorized response
readHTML = false; // Sets readHTML to false
return readHTML; // Ends the function early
}
// Checks if the line starts with a relevant variable
if (line.startsWith("var webdata") || line.startsWith("var cover") || line.startsWith("var status")) {
line.replace("var ", ""); // Removes "var " from the start of the line
response += line + "\n"; // Adds the line to the response
}
line = ""; // Resets the line
} else if (c != '\r') { // If the character is not a carriage return
line += c; // Adds the character to the line
}
}
}
ParseStatus result = inverter.updateData(response); // Parses and updates the data
client.stop(); // Stops the connection to the server
} else {
// Logic could be added here to handle failed connections
// No connection possible
}
return readHTML; // Returns whether the HTML was successfully read
}
void handleInverterNetworkMode() {
// Check if the connection to the inverter network was successful
if (connectedToInverterNetwork && (WiFi.status() == WL_CONNECTED)) {
// Establish a UDP connection to set the correct time
bool udpConnectionCreated = inverterUdp.inverter_connect(WiFi.gatewayIP().toString(), udpServerPort, udpLocalPort, udpTimeout_s);
if (udpConnectionCreated) {
String response = inverterUdp.inverter_readtime(); // Reads the time from the inverter
if (inverterUdp.isDefaultTimeSet()) { // Checks if the standard time is set and adjusts if necessary
response = inverterUdp.inverter_settime(getCurrentEpochTime()); // Sets the current time
String response_new = inverterUdp.inverter_readtime(); // Reads the newly set time
}
inverterUdp.inverter_close(); // Closes the UDP session
} else {
// No UDP connection possible
// ...
}
// Read and parse data from the inverter's HTML page
bool readHTML = readInverterDataFromWebInterface(status_page_url, INVERTER_WEBACCESS_USER, INVERTER_WEBACCESS_PWD);
if (readHTML) {
newInverterDataAvailable = true; // New data is available
lastInverterUpdateMillis = millis();
}
inverter.printVariables(); // Outputs the inverter status on the Serial Monitor
} else {
if ((millis() - lastInverterUpdateMillis > INVERTER_OFFLINE_TIMEOUT_SECONDS * 1000)) {
inverter.setInactiveValues(); // Sets inactive values if the inverter is not reachable
newInverterDataAvailable = true; // Indicates that new data is available
}
if (hour() == 0 && inverter.getInverterEnergyToday_kWh() > 0.0) {
inverter.resetEnergyTodayCounter(); // Resets the daily counter at midnight
newInverterDataAvailable = true; // Indicates that new data is available
}
}
currentState = HOME_NETWORK_MODE; // Switches back to the home network mode
wifi_connect(WIFI_HOME_SSID, WIFI_HOME_KEY, "Home WiFi"); // Connects to the home WiFi
energyDisplay.start(); // Starts the display
}void handleHomeNetworkMode() {
if (connectedToHomeNetwork && (WiFi.status() == WL_CONNECTED)) {
homeNetworkNotReachableCount = 0; // Resets the counter for network unreachability
webServerManager.handleClient(); // Handles incoming web server requests
if (newInverterDataAvailable) {
mqttManager->publishAllData(); // Publishes the new inverter data via MQTT
newInverterDataAvailable = false; // Resets the flag for new data
}
}
// Check if a switch to another mode is necessary
if (cndHomeNetworkToAPNetwork()) {
currentState = AP_MODE; // Switches to AP mode if the home network is not found
activateAPMode();
} else if (cndHomeNetworkToInverterNetwork()) {
wifi_connect(WIFI_INVERTER_SSID, WIFI_INVERTER_KEY, "Inverter WiFi"); // Connects to the inverter WiFi
currentState = INVERTER_NETWORK_MODE;
}
}void handleAPMode() {
webServerManager.handleClient(); // Handles incoming web server requests
// Displaying access information on the display
action.name = "AP Online";
action.details = "Connect using";
action.params[0] = "SSID: " + String(WIFI_AP_NAME);
String ip_string = WiFi.softAPIP().toString();
action.params[1] = "PSWD: " + String(WIFI_AP_PASSWORD);
action.params[2] = "IP: " + ip_string;
displayManager.displayAction(action); // Displays the action on the display
if (cndAPToHomeNetwork()) {
deactivateAPMode(); // Deactivates AP mode
activatedAP = false;
action.name = "AP Stop";
action.details = "Ended";
displayManager.displayAction(action); // Updates the display with the current status
currentState = HOME_NETWORK_MODE; // Switches to home network mode
wifi_connect(WIFI_HOME_SSID, WIFI_HOME_KEY, "Home WiFi"); // Activates the home network
energyDisplay.start(); // Starts the display updates
}
}
This section covers the essential functions of the u8g2 library used for controlling displays. Below are the descriptions and examples of each function.
Sets the font used for text display. The u8g2 library supports a variety of fonts in different sizes and styles.
u8g2.setFont(font);Draws a string at the specified position (startX, lowerY), where lowerY represents the lower edge of the text. This function is useful for precisely positioning text on the display.
u8g2.drawStr(startX, lowerY, "Hallo Display");Returns the width of a string, depending on the currently set font. This function does not draw the string, but is useful for layout calculations, such as centering text or determining if a text fits within a certain area.
u8g2.getStrWidth("Hallo Welt");Clears the contents of the display buffer. This action removes everything that was previously drawn, allowing you to start with a clean screen. It is particularly useful for preparing the display for the next viewing without leaving remnants of previous content.
u8g2.clearBuffer();Sends the update to the display. This function transfers the contents of the buffer to the display, displaying all the contents that were added using the drawing functions. It is important to call this function after you are done drawing the screen content to make the changes visible.
u8g2.sendBuffer();OLED modules and the boards they are mounted on can vary significantly. The variations primarily include pixel dimensions, I2C addresses (sometimes printed on the device and adjustable), and the specific pins used for the I2C bus. Some boards use the hardware-designated I2C pins, while others use arbitrary pins. Additionally, there is sometimes conflicting information available for certain boards.
The u8g2 library has proven extremely helpful in managing these variations more flexibly than the Adafruit Graphics Library. Unlike Adafruit's library, which allowed limited adjustments and only operated under different I2C addresses at half resolution, u8g2 generally does not even require the specification of the I2C address. This feature is particularly beneficial for setups involving multiple displays, where manually setting the I2C address becomes necessary.
Below is an implementation of the DisplayManager constructor that initializes display objects based on the board type. This constructor utilizes conditional compilation to adapt to different hardware setups.
DisplayManager::DisplayManager(SerialCaptureLines& serialCapture): serialCapture(serialCapture) {
// Conditional initialization based on board type
#ifdef BOARD_WEMOS_OLED_128x64_ESP32
u8g2 = new U8G2_SSD1306_128X64_NONAME_F_SW_I2C(U8G2_R0, /* clock=*/ 4, /* data=*/ 5, /* reset=*/ U8X8_PIN_NONE);
#elif defined(BOARD_HELTEC_OLED_128x32_ESP8266)
u8g2 = new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ SCL, /* data=*/ SDA);
#elif defined(BOARD_HELTEC_WiFiKit_32_V3_OLED_128x32_ESP32)
u8g2 = new U8G2_SSD1306_128X64_NONAME_F_SW_I2C(U8G2_R0, /* clock=*/ 18, /* data=*/ 17, /* reset=*/ 21);
#elif defined(BOARD_WEMOS_OLED_128x32_ESP32_S2)
u8g2 = new U8G2_SSD1306_128X32_UNIVISION_F_SW_I2C (U8G2_R0, /* clock=*/ SCL, /* data=*/ SDA, /* reset=*/ 18);
#elif defined(BOARD_ESP8266_OLED_Black_128x64_SDA_D1__SDC_D2)
u8g2 = new U8G2_SSD1306_128X64_NONAME_F_SW_I2C(U8G2_R0, /* clock=*/ 2, /* data=*/ 1, /* reset=*/ U8X8_PIN_NONE);
#else
u8g2 = new U8G2_SSD1306_128X64_NONAME_F_SW_I2C(U8G2_R0, /* clock=*/ SCL, /* data=*/ SDA, /* reset=*/ U8X8_PIN_NONE);
#endif
}This constructor is designed to accommodate a variety of boards by setting the correct pin configurations and I2C addresses as needed.
The ActionData structure is used to pass data for an action, encapsulating the name, details, up to three lines of additional information (parameters), the result, and details about the result.
struct ActionData {
String name;
String details;
String params[3]; // Array to hold up to 3 lines of information
String result;
String resultDetails;
};The DisplayManager::displayAction method is responsible for displaying the details of an action on a screen. It uses the u8g2 library for drawing text and shapes on the display.
void DisplayManager::displayAction(const ActionData& action) {
int yPositionBottomLine = SCREEN_HEIGHT - 1; // Calculate the Y position for the bottom line
String actionStr(action.name.c_str()); // Convert the action name to a String
u8g2->clearBuffer(); // Clears the display buffer
u8g2->setFont(displayActionFontHeader); // Sets the font for the header
int actionNameWidth = u8g2->getStrWidth(action.name.c_str()); // Calculate the width of the action name
int startX = (SCREEN_WIDTH - actionNameWidth) / 2; // Calculate the starting point for centered text
u8g2->drawStr(startX, lowerY_FirstLine, action.name.c_str()); // Draw the action name centered
u8g2->setFont(displayActionFontBody); // Sets the font for body text
u8g2->drawStr(0, lowerY_SecondLine, action.details.c_str()); // Draw the action details
// Loop through all parameters and draw them if they exist
for (int i = 0; i < 3 && i < sizeof(action.params) / sizeof(action.params[0]); ++i) {
if (action.params[i].length() > 0) {
u8g2->drawStr(0, lowerY_ThirdLine + (i * 9), action.params[i].c_str()); // Draw each parameter
}
}
u8g2->setFont(displayActionFontBodySmaller); // Sets a smaller font for the result text
int resultWidth = u8g2->getStrWidth(action.result.c_str()) + 8; // Calculate the width of the result text including margin
int resultStartX = 0; // Set the start position for the result
u8g2->setDrawColor(1); // Sets the draw color for the background rectangle
u8g2->drawBox(resultStartX, yPositionBottomLine - 8, resultWidth, 10); // Draw a background rectangle
u8g2->setDrawColor(0); This method handles all the graphical operations needed to display the action's data on the screen, providing clear visual feedback to the user.
-
Initialize ActionData Object First, create an instance of
ActionDataand set its initial properties. This includes the action name, details, parameters like broker and port, and an initial result state.ActionData action; action.name = "MQTT Sync"; action.details = "Sende Daten"; // "Sending Data" action.params[0] = "Broker: " + String(_broker); action.params[1] = "Port: " + String(_port); action.result = "In Arbeit"; // "In Progress"
-
Display Initial Action State Display the initial state of the action using the
displayActionmethod of theDisplayManagerinstance.displayManager.displayAction(action); // Display the data on the screen -
Perform Actions After displaying the initial state, perform the necessary actions such as data transmission or other processing steps.
// Actions are performed here... -
Update and Display Final State Once the actions are completed, update the result and result details of the
ActionDataobject. Then, display the final state again.action.result = "Done"; // Update the result action.resultDetails = "Published"; // Add details to the result displayManager.displayAction(action); // Display the updated data on the screen
The displayAction method is used to display an action's current state and details on a screen. This example demonstrates how to use the ActionData struct to send data to the displayManager for display at different stages of an action, such as during an MQTT synchronization process.
This method is designed to display a large number along with its unit and an annotation on the display. It dynamically adjusts font sizes to ensure the number and unit fit within the display width.
void drawBigNumberNoHeader(float number, String unit, String annotation, String formattingStr);float number: The numeric value to display.String unit: The unit of the number (e.g., "kg", "m").String annotation: A brief note or label to display above the number.String formattingStr: A C-style string formatting pattern for the number.
- Font Initialization: The method starts by setting default fonts for the number and the unit.
- Number Formatting: Uses
snprintfto format the number into a string based on the provided formatting pattern. - Width Calculation and Font Adjustment: Calculates the width of the formatted number and unit. If the combined width exceeds the display width, it switches to smaller fonts for both.
- Positioning: Calculates the position of the unit to align it to the right edge of the display and positions the number to the left of the unit with a small gap.
- Drawing:
- Draws the annotation at a fixed position.
- Draws the unit and number using the adjusted or default fonts.
- Display Update: Finally, it updates the display to show the drawn elements.
void DisplayManager::drawBigNumberNoHeader(float number, String unit, String annotation, String formattingStr) {
const uint8_t * currentNumberFont = numberFont;
const uint8_t* currentUnitFont = unitFont;
char numberBuffer[20];
snprintf(numberBuffer, sizeof(numberBuffer), formattingStr.c_str(), number);
u8g2->setFont(currentNumberFont);
int numberWidth = u8g2->getStrWidth(numberBuffer);
u8g2->setFont(currentUnitFont);
int unitWidth = u8g2->getStrWidth(unit.c_str());
if ((numberWidth + unitWidth + 4) > 128) {
currentNumberFont = smallNumberFont;
currentUnitFont = smallUnitFont;
u8g2->setFont(currentNumberFont);
numberWidth = u8g2->getStrWidth(numberBuffer);
u8g2->setFont(currentUnitFont);
unitWidth = u8g2->getStrWidth(unit.c_str());
}
int unitX = 128 - unitWidth;
int unitY = SCREEN_HEIGHT - 1;
int numberX = unitX - numberWidth - 4;
int numberY = unitY;
u8g2->setFont(bigNumberAnnotationFont);
u8g2->drawStr(0, 28, annotation.c_str());
u8g2->setFont(currentUnitFont);
u8g2->drawStr(unitX, unitY, unit.c_str());
u8g2->setFont(currentNumberFont);
u8g2->drawStr(numberX, numberY, numberBuffer);
u8g2->sendBuffer();
}This method is particularly useful for applications requiring the display of measurements or data points in a visually significant and clear manner, ensuring readability even from a distance.
The Inverter class is designed to manage and parse data for an inverter, typically used in solar power systems. It handles the extraction of real-time data from HTML content, updates internal state variables, and logs activities via a serial interface.
Prints current variables related to the inverter to the serial output for debugging and verification purposes.
void Inverter::printVariables() const {
serialCapture.println("Inverter Variables:");
serialCapture.print("Webdata SN: "); serialCapture.println(webdata_sn);
serialCapture.print("Webdata Rate P: "); serialCapture.println(webdata_rate_p);
serialCapture.print("Last Update Timestamp: "); serialCapture.println(lastUpdateTimestamp);
}Updates inverter data by parsing HTML content. This method uses the extractVariables function to parse the data and update internal variables.
ParseStatus Inverter::updateData(const String& html) {
serialCapture.println("Start updating data");
ParseStatus result = extractVariables(html);
return result;
}Analyzes HTML content to extract data and store it in class variables. It handles both string and float values, ensuring data integrity and updating timestamp markers upon successful data extraction.
ParseStatus Inverter::extractVariables(const String& html) {
if (html.isEmpty()) {
serialCapture.println("HTML is empty");
return PARSE_HTML_ISSUES;
}
int countParseErrors = 0;
float temp_webdata_now_p = extractFloatValue(html, "webdata_now_p =");
if (temp_webdata_now_p <= 0.0) {
serialCapture.println("webdata_now_p is <= 0.0, indicating parsing issues");
return PARSE_PARSING_ISSUES;
}
String temp_webdata_sn = extractAndValidateString(html, "webdata_sn =", countParseSuccess);
if (countParseErrors == 0) {
webdata_now_p = temp_webdata_now_p;
lastSuccessfullTimestamp = millis();
lastUpdateTimestamp = lastSuccessfullTimestamp;
lastReadSuccess = true;
return PARSE_OK;
} else {
lastUpdateTimestamp = millis();
lastReadSuccess = false;
return PARSE_PARSING_ISSUES;
}
}Generic functions used within extractVariables to locate and convert values from HTML.
String Inverter::extractValue(const String& html, const String& variableName) const {
int startIndex = html.indexOf(variableName);
if (startIndex != -1) {
startIndex += variableName.length();
int endIndex = html.indexOf(";", startIndex);
if (endIndex != -1) {
String value = html.substring(startIndex, endIndex);
value.replace("\"", "");
value.trim();
return value;
}
}
return "";
}
float Inverter::extractFloatValue(const String& html, const String& variableName) const {
String valueStr = extractValue(html, variableName);
if (valueStr.length > 0) {
return valueStr.toFloat();
}
return 0.0;
}The Inverter class serves as a critical component for managing and interpreting data from inverters in solar power systems, ensuring data accuracy and providing real-time updates for system monitoring and management.
The MQTTManager class handles MQTT communication for a solar inverter system. It manages MQTT connections, publishes sensor data, and configures Home Assistant autodiscovery to automatically detect and use devices and sensors.
The constructor initializes an MQTTManager instance with necessary credentials and references to other system components.
MQTTManager::MQTTManager(const char* broker, uint16_t port, const char* user, const char* pwd, DisplayManager& displayManager, Inverter& inverter, SerialCaptureLines& serialCapture)
: _broker(broker), _port(port), _user(user), _pwd(pwd), _displayManager(displayManager), _inverter(inverter), mqttClient(espClient), serialCapture(serialCapture) {
mqttClient.setServer(_broker, _port);
mqttClient.setBufferSize(1024); // Note: Normally the buffer size is much smaller.
}Publishes data to a specified MQTT topic.
void MQTTManager::publish(const char* topic, const char* payload);Publishes all relevant inverter data to their respective MQTT topics and handles Home Assistant autodiscovery messages.
void MQTTManager::publishAllData() {
if (mqttClient.connected()) {
bool publishSuccess;
// Publish Home Assistant autodiscovery data for sensors
publishSuccess = publish("homeassistant/sensor/solar_inverter/power/config", configPower, true);
publishSuccess = publish("homeassistant/sensor/solar_inverter/energy_today/config", configEnergyToday, true);
publishSuccess = publish("homeassistant/sensor/solar_inverter/energy_total/config", configEnergyTotal, true);
// Publish real-time inverter data
publish("SolarInverterBridge/inverter/serial", _inverter.getInverterSerial().c_str());
publish("SolarInverterBridge/inverter/power_W", String(_inverter.getInverterPowerNow_W()).c_str());
publish("SolarInverterBridge/inverter/energy_today_kWh", String(_inverter.getInverterEnergyToday_kWh()).c_str());
publish("SolarInverterBridge/inverter/energy_total_kWh", String(_inverter.getInverterEnergyTotal_kWh()).c_str());
// Handle other tasks...
disconnect();
} else {
serialCapture.println("[DBG] MQTT Connection not available.");
}
}The following JSON configurations are used for integrating sensors with Home Assistant via MQTT autodiscovery:
- Power Sensor Configuration
{
"unique_id": "solar_inverter_power",
"device_class": "power",
"icon": "mdi:solar-power",
"device": {
"identifiers": ["deye_600_12345678"],
"name": "Solar Inverter",
"model": "EU 600W",
"via_device": "esp-mqtt-bridge",
"manufacturer": "Deye"
},
"name": "Solar Power",
"state_topic": "SolarInverterBridge/inverter/power_W",
"unit_of_measurement": "W",
"value_template": "{{ value }}"
}- Energy Today Sensor Configuration
{
"unique_id": "solar_inverter_energy_today",
"device_class": "energy",
"icon": "mdi:solar-panel",
"device": {
"identifiers": ["deye_600_12345678"],
"name": "Solar Inverter",
"model": "EU 600W",
"via_device": "esp-mqtt-bridge",
"manufacturer": "Deye"
},
"name": "Solar Energy Today",
"state_topic": "SolarInverterBridge/inverter/energy_today_kWh",
"unit_of_measurement": "kWh",
"value_template": "{{ value }}"
}- Energy Total Sensor Configuration
{
"unique_id": "solar_inverter_energy_total",
"device_class": "energy",
"icon": "mdi:sigma",
"device": {
"identifiers": ["deye_600_12345678"],
"name": "Solar Inverter",
"model": "EU 600W",
"via_device": "esp-mqtt-bridge",
"manufacturer": "Deye"
},
"name": "Solar Energy Total",
"state_topic": "SolarInverterBridge/inverter/energy_total_kWh",
"unit_of_measurement": "kWh",
"value_template": "{{ value }}"
}The MQTTManager ensures the inverter's seamless integration into smart home systems, providing real-time updates and enabling remote monitoring and control via Home Assistant.



