Zum Inhalt

API_INTEGRATION.md - SolarLog API Documentation

Integration guide for SolarLog API (https://solarlog-api.karma.organic)


🌐 API Overview

Base URL: https://solarlog-api.karma.organic
Documentation: https://solarlog-api.karma.organic/docs
Protocol: HTTPS (TLS 1.2+)
Format: JSON (application/json)
Authentication: TBD (check API docs for Bearer token / API key)


πŸ” Authentication

⚠️ TODO: Research Required

Check the API documentation to determine: 1. Authentication Method: Bearer token, API key, OAuth2? 2. Token Acquisition: How to obtain API credentials? 3. Token Lifespan: Does token expire? Refresh mechanism? 4. Rate Limiting: Requests per minute/hour limit?

Placeholder Implementation

// firmware/src/network/solarlog_api.h
#define SOLARLOG_API_KEY "your-api-key-here"  // Store in NVS
#define SOLARLOG_API_URL "https://solarlog-api.karma.organic/api/v1"

// Example: Bearer token authentication (TBD)
void api_set_auth_header(HTTPClient &http) {
    String auth = "Bearer " + String(SOLARLOG_API_KEY);
    http.addHeader("Authorization", auth);
    http.addHeader("Content-Type", "application/json");
}

πŸ“‘ API Endpoints

⚠️ Endpoints are placeholder - verify with actual API docs!

1. POST /api/v1/inverters/{inverter_id}/data

Purpose: Submit solar production data from inverter

Method: POST
URL: https://solarlog-api.karma.organic/api/v1/inverters/{inverter_id}/data

Headers:

Authorization: Bearer {api_key}
Content-Type: application/json

Request Body:

{
  "timestamp": "2025-10-23T09:30:00Z",
  "power_w": 3500,
  "energy_kwh": 12.5,
  "voltage_v": 230,
  "current_a": 15.2,
  "status": "online",
  "temperature_c": 45.3
}

Response (200 OK):

{
  "success": true,
  "message": "Data received",
  "data_id": "abc123def456"
}

Response (400 Bad Request):

{
  "success": false,
  "error": "Invalid timestamp format"
}

Response (401 Unauthorized):

{
  "success": false,
  "error": "Invalid API key"
}


2. GET /api/v1/inverters/{inverter_id}/status

Purpose: Check inverter status and last data point

Method: GET
URL: https://solarlog-api.karma.organic/api/v1/inverters/{inverter_id}/status

Response (200 OK):

{
  "inverter_id": "inv-001",
  "status": "online",
  "last_update": "2025-10-23T09:25:00Z",
  "power_w": 3500,
  "energy_today_kwh": 12.5
}


3. GET /api/v1/inverters/{inverter_id}/history

Purpose: Retrieve historical data (last 7/30 days)

Method: GET
URL: https://solarlog-api.karma.organic/api/v1/inverters/{inverter_id}/history?days=7

Query Parameters: - days: Number of days (1-365) - resolution: hourly | daily (optional)

Response (200 OK):

{
  "inverter_id": "inv-001",
  "days": 7,
  "data": [
    {"date": "2025-10-17", "energy_kwh": 18.3},
    {"date": "2025-10-18", "energy_kwh": 22.1},
    {"date": "2025-10-19", "energy_kwh": 19.8},
    {"date": "2025-10-20", "energy_kwh": 21.5},
    {"date": "2025-10-21", "energy_kwh": 20.2},
    {"date": "2025-10-22", "energy_kwh": 23.7},
    {"date": "2025-10-23", "energy_kwh": 12.5}
  ]
}


πŸ’» ESP32 Implementation

HTTP Client (HTTPS)

// firmware/src/network/solarlog_api.cpp
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "solarlog_api.h"

WiFiClientSecure secureClient;
HTTPClient http;

bool api_init() {
    // Set root CA certificate (for HTTPS validation)
    // TODO: Add karma.organic root CA cert
    // secureClient.setCACert(root_ca_cert);

    // OR: Skip certificate validation (NOT recommended for production)
    secureClient.setInsecure();

    return true;
}

bool api_post_data(float power_w, float energy_kwh, const char* status) {
    String url = String(SOLARLOG_API_URL) + "/inverters/inv-001/data";

    http.begin(secureClient, url);
    http.addHeader("Authorization", "Bearer " + String(SOLARLOG_API_KEY));
    http.addHeader("Content-Type", "application/json");

    // Create JSON payload
    StaticJsonDocument<256> doc;
    doc["timestamp"] = get_iso8601_timestamp(); // Helper function
    doc["power_w"] = power_w;
    doc["energy_kwh"] = energy_kwh;
    doc["status"] = status;

    String payload;
    serializeJson(doc, payload);

    // Send POST request
    int httpCode = http.POST(payload);

    if (httpCode == 200) {
        String response = http.getString();
        Serial.println("βœ… API POST success: " + response);
        http.end();
        return true;
    } else {
        Serial.printf("❌ API POST failed: %d - %s\n", httpCode, http.errorToString(httpCode).c_str());
        http.end();
        return false;
    }
}

String get_iso8601_timestamp() {
    // Get current time from NTP or RTC
    time_t now;
    time(&now);

    char buffer[25];
    strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", gmtime(&now));
    return String(buffer);
}

πŸ›‘οΈ Error Handling

Retry Logic

bool api_post_with_retry(float power, float energy, int max_retries = 3) {
    for (int attempt = 1; attempt <= max_retries; attempt++) {
        Serial.printf("πŸ“€ API POST attempt %d/%d\n", attempt, max_retries);

        if (api_post_data(power, energy, "online")) {
            return true;  // Success
        }

        // Exponential backoff: 2s, 4s, 8s
        int delay_ms = (1 << attempt) * 1000;
        Serial.printf("⏳ Retry in %d ms...\n", delay_ms);
        delay(delay_ms);
    }

    Serial.println("❌ API POST failed after all retries");
    return false;
}

Network Error Handling

typedef enum {
    API_SUCCESS,
    API_ERR_NETWORK,       // WiFi not connected
    API_ERR_TIMEOUT,       // HTTP timeout
    API_ERR_AUTH,          // 401 Unauthorized
    API_ERR_BAD_REQUEST,   // 400 Bad Request
    API_ERR_SERVER,        // 500 Server Error
    API_ERR_UNKNOWN
} api_error_t;

api_error_t api_post_data_ex(float power, float energy) {
    // Check WiFi first
    if (WiFi.status() != WL_CONNECTED) {
        return API_ERR_NETWORK;
    }

    // ... HTTP request ...

    int httpCode = http.POST(payload);

    if (httpCode == 200) return API_SUCCESS;
    else if (httpCode == 401) return API_ERR_AUTH;
    else if (httpCode == 400) return API_ERR_BAD_REQUEST;
    else if (httpCode >= 500) return API_ERR_SERVER;
    else if (httpCode < 0) return API_ERR_TIMEOUT;
    else return API_ERR_UNKNOWN;
}

πŸ“Š Data Caching (Offline Mode)

Store failed uploads in NVS

// Cache data locally if API upload fails
void cache_data_point(float power, float energy) {
    Preferences prefs;
    prefs.begin("cache", false);

    // Store up to 100 data points (circular buffer)
    int count = prefs.getInt("count", 0);
    String key = "dp_" + String(count % 100);

    StaticJsonDocument<128> doc;
    doc["t"] = millis();
    doc["p"] = power;
    doc["e"] = energy;

    String data;
    serializeJson(doc, data);

    prefs.putString(key.c_str(), data);
    prefs.putInt("count", count + 1);
    prefs.end();

    Serial.printf("πŸ’Ύ Cached data point #%d\n", count);
}

// Retry uploading cached data when online
void flush_cached_data() {
    Preferences prefs;
    prefs.begin("cache", false);

    int count = prefs.getInt("count", 0);
    if (count == 0) {
        prefs.end();
        return;  // Nothing to flush
    }

    Serial.printf("πŸ“€ Flushing %d cached data points...\n", count);

    for (int i = 0; i < min(count, 100); i++) {
        String key = "dp_" + String(i);
        String data = prefs.getString(key.c_str(), "");

        if (data.length() > 0) {
            // Parse and upload
            StaticJsonDocument<128> doc;
            deserializeJson(doc, data);

            if (api_post_data(doc["p"], doc["e"], "cached")) {
                prefs.remove(key.c_str());  // Delete on success
            }
        }
    }

    prefs.putInt("count", 0);  // Reset counter
    prefs.end();
}

πŸ•’ Update Intervals

Interval Use Case Battery Life Impact
5 min Real-time monitoring ~30 days
15 min Normal operation ~90 days
30 min Battery saving mode ~180 days
60 min Minimal updates ~360 days

Implementation

#define UPDATE_INTERVAL_MS (5 * 60 * 1000)  // 5 minutes

void loop() {
    static unsigned long last_update = 0;

    if (millis() - last_update > UPDATE_INTERVAL_MS) {
        // 1. Read inverter data
        float power = inverter_get_power();
        float energy = inverter_get_energy();

        // 2. POST to API
        if (api_post_with_retry(power, energy)) {
            Serial.println("βœ… Data uploaded");
        } else {
            Serial.println("❌ Upload failed, caching...");
            cache_data_point(power, energy);
        }

        // 3. Update display
        screen_dashboard_update(power, energy);

        last_update = millis();
    }

    lv_timer_handler();
    delay(10);
}

πŸ§ͺ Testing

1. Mock API Server (Development)

Use Postman or httpbin for testing:

# Test POST with httpbin.org
curl -X POST https://httpbin.org/post \
  -H "Authorization: Bearer test-key-123" \
  -H "Content-Type: application/json" \
  -d '{
    "timestamp": "2025-10-23T10:00:00Z",
    "power_w": 3500,
    "energy_kwh": 12.5
  }'

2. ESP32 Serial Monitor

void test_api() {
    Serial.println("πŸ§ͺ Testing API connection...");

    if (api_post_data(3500, 12.5, "online")) {
        Serial.println("βœ… API test PASSED");
    } else {
        Serial.println("❌ API test FAILED");
    }
}

πŸ“ž Troubleshooting

Issue: 401 Unauthorized

  • βœ… Verify API key is correct
  • βœ… Check if key is expired
  • βœ… Ensure Authorization header format is correct

Issue: SSL/TLS Handshake Failed

  • βœ… Check if secureClient.setInsecure() is enabled (for testing)
  • βœ… Add root CA certificate for karma.organic domain
  • βœ… Verify ESP32 has correct date/time (NTP sync required)

Issue: Timeout

  • βœ… Check WiFi signal strength (RSSI)
  • βœ… Increase HTTP timeout: http.setTimeout(10000) (10s)
  • βœ… Verify API endpoint URL is correct

πŸ“š Resources

  • API Documentation: https://solarlog-api.karma.organic/docs
  • ESP32 HTTPClient: https://github.com/espressif/arduino-esp32/tree/master/libraries/HTTPClient
  • ArduinoJson: https://arduinojson.org/v6/doc/

⚠️ CRITICAL TODO: 1. Visit https://solarlog-api.karma.organic/docs 2. Obtain API credentials (API key or OAuth token) 3. Update authentication code in solarlog_api.cpp 4. Test with real API endpoint 5. Document actual request/response format


Last Updated: 2025-10-23
API Version: TBD
Status: 🚧 Placeholder - Requires API Documentation Review