Zum Inhalt

System-Architektur

Übersicht

Das SolarLog-System ist eine modulare Web-Anwendung zur Überwachung und Verwaltung von Solar-Wechselrichtern. Es folgt einer Drei-Schichten-Architektur mit klarer Trennung von Frontend, Backend und Datenhaltung.

High-Level Architektur

┌──────────────────────────────────────────────────────────────┐
│                        Frontend Layer                         │
│  ┌────────────────────────────────────────────────────────┐  │
│  │              React Application (Port 3002)             │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │  │
│  │  │  Dashboard   │  │  SetupPage   │  │   Dialogs    │ │  │
│  │  └──────────────┘  └──────────────┘  └──────────────┘ │  │
│  │  ┌──────────────────────────────────────────────────┐ │  │
│  │  │          API Service Layer (inverterAPI.ts)      │ │  │
│  │  └──────────────────────────────────────────────────┘ │  │
│  └────────────────────────────────────────────────────────┘  │
└───────────────────────────┬──────────────────────────────────┘
                            │ HTTP/REST
┌───────────────────────────┼──────────────────────────────────┐
│                           ▼         Backend Layer            │
│  ┌────────────────────────────────────────────────────────┐  │
│  │           Express Server (Port 3000)                   │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │  │
│  │  │   Inverter   │  │     Scan     │  │   Legacy     │ │  │
│  │  │    Routes    │  │    Routes    │  │   Routes     │ │  │
│  │  └──────────────┘  └──────────────┘  └──────────────┘ │  │
│  │  ┌──────────────────────────────────────────────────┐ │  │
│  │  │         DatabaseManager (db.js)                  │ │  │
│  │  └──────────────────────────────────────────────────┘ │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                               │
│  ┌────────────────────────────────────────────────────────┐  │
│  │          Scanner Service (Port 3001)                   │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │  │
│  │  │Network Scan  │  │Device Detect │  │   Results    │ │  │
│  │  └──────────────┘  └──────────────┘  └──────────────┘ │  │
│  └────────────────────────────────────────────────────────┘  │
└───────────────────────────┬──────────────────────────────────┘
                            │ SQLite
┌───────────────────────────┼──────────────────────────────────┐
│                           ▼          Data Layer              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │                SQLite Database                         │  │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │  │
│  │  │inverters │ │  scans   │ │production│ │  errors  │ │  │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘ │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

Frontend-Architektur (React)

Komponenten-Hierarchie

App.tsx
├── Router (BrowserRouter)
│   ├── AppBar (Navigation)
│   └── Routes
│       ├── Dashboard (/)
│       │   ├── CurrentPower
│       │   ├── DailyChart
│       │   └── TotalProduction
│       └── SetupPage (/setup)
│           ├── InverterTable
│           ├── InverterDialog
│           └── ScanDialog

State Management

Aktuell nutzt das System lokalen Component State und API-Aufrufe. Für komplexere Anwendungen könnte Redux oder Context API integriert werden.

// Beispiel: SetupPage State
const [inverters, setInverters] = useState<Inverter[]>([]);
const [loading, setLoading] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);

API-Service Layer

// inverterAPI.ts
export const inverterAPI = {
    getAll: () => axios.get('/api/inverters'),
    getOne: (id) => axios.get(`/api/inverters/${id}`),
    create: (data) => axios.post('/api/inverters', data),
    update: (id, data) => axios.put(`/api/inverters/${id}`, data),
    delete: (id) => axios.delete(`/api/inverters/${id}`)
};

Vorteile: - Zentrale Verwaltung aller API-Aufrufe - Einfache Anpassung bei API-Änderungen - Typsicherheit durch TypeScript - Wiederverwendbarkeit

Backend-Architektur (Express)

Schichten-Modell

┌─────────────────────────────────────────┐
│         Routes Layer (REST API)         │
│  /api/inverters, /api/scan, /api/v1    │
└──────────────────┬──────────────────────┘
┌──────────────────▼──────────────────────┐
│       Business Logic Layer              │
│  Validation, Error Handling, Transform │
└──────────────────┬──────────────────────┘
┌──────────────────▼──────────────────────┐
│         Data Access Layer               │
│     DatabaseManager (CRUD Operations)   │
└──────────────────┬──────────────────────┘
┌──────────────────▼──────────────────────┐
│            Database Layer               │
│              SQLite3                    │
└─────────────────────────────────────────┘

Route-Struktur

// server.js
app.use('/api/inverters', inverterRoutes);
app.use('/api/scan', scanRoutes);
app.use('/api/v1', legacyRoutes);

Routes-Dateien: - src/routes/inverters.js: Wechselrichter-Verwaltung - src/routes/scan.js: Scan-Ergebnisse - Legacy-Routes direkt in server.js

Database Manager Pattern

class DatabaseManager {
    constructor() {
        this.db = new Database(dbPath);
        this.initialize();
    }

    // Singleton Pattern
    static getInstance() {
        if (!instance) {
            instance = new DatabaseManager();
        }
        return instance;
    }

    // CRUD Methoden
    createInverter(data) { /* ... */ }
    getInverter(id) { /* ... */ }
    updateInverter(id, data) { /* ... */ }
    deleteInverter(id) { /* ... */ }
}

Vorteile: - Zentrale Datenbank-Instanz (Singleton) - Automatische Initialisierung - Einheitliche Error-Handling - Vorbereitet für Prepared Statements

Scanner-Service-Architektur

Scan-Workflow

Start Scan
Detect Network Interfaces
For each subnet:
    ├─► Try nmap (if available)
    │       └─► Port scan (80,81,502,1502)
    └─► Fallback: Simple scan
            └─► Check ports sequentially
For each discovered device:
    ├─► Try Modbus connection (Port 502)
    │       └─► Read device info
    └─► Try HTTP connection (Port 80/81)
            └─► Analyze HTML for manufacturer
Save to database (scan_results)
Return results

Geräte-Identifikation

const MANUFACTURER_SIGNATURES = {
    SMA: ['SMA', 'Sunny'],
    KOSTAL: ['KOSTAL', 'PIKO'],
    DELTA: ['DELTA', 'Solivia']
};

async function identifyDevice(ip, ports) {
    // 1. Modbus-basierte Identifikation
    if (ports.includes(502)) {
        try {
            const modbus = new ModbusRTU();
            await modbus.connectTCP(ip);
            const data = await modbus.readHoldingRegisters(40000, 10);
            return parseManufacturer(data);
        } catch (e) { /* fallback */ }
    }

    // 2. HTTP-basierte Identifikation
    if (ports.includes(80)) {
        const html = await fetch(`http://${ip}`).text();
        return findSignature(html, MANUFACTURER_SIGNATURES);
    }
}

Datenbank-Architektur

Schema-Design

inverters (Master-Daten)
    ├──► production_data (1:n)
    │       └─ Historische Messwerte
    ├──► error_log (1:n)
    │       └─ Fehlerprotokoll
scan_results (Unabhängig)
    └─ Netzwerk-Scan-Ergebnisse

settings (Key-Value Store)
    └─ System-Konfiguration

Indizes für Performance

-- Häufige Abfragen optimieren
CREATE INDEX idx_inverters_ip_address ON inverters(ip_address);
CREATE INDEX idx_production_data_inverter ON production_data(inverter_id);
CREATE INDEX idx_production_data_timestamp ON production_data(timestamp);
CREATE INDEX idx_error_log_timestamp ON error_log(timestamp);

Trigger für Automatisierung

-- Auto-Update Timestamps
CREATE TRIGGER update_inverters_timestamp 
AFTER UPDATE ON inverters
BEGIN
    UPDATE inverters 
    SET updated_at = CURRENT_TIMESTAMP 
    WHERE id = NEW.id;
END;

Kommunikations-Fluss

Inverter hinzufügen

User Action (Frontend)
InverterDialog.handleSave()
inverterAPI.create(data)
HTTP POST /api/inverters
inverterRoutes.post('/')
db.createInverter(data)
SQLite INSERT INTO inverters
Return new inverter
Update UI

Netzwerk-Scan

User Action (Frontend)
ScanDialog.handleScan()
networkScanAPI.startScan()
HTTP POST http://localhost:3001/scan
Scanner Service: scanNetwork()
For each device:
    identifyDevice() → db.saveScanResult()
SQLite INSERT INTO scan_results
Return to cache
Frontend polls: GET /scan
Display results

Sicherheitsarchitektur

Aktuelle Implementierung

  • CORS aktiviert für localhost:3000, 3001, 3002
  • Keine Authentifizierung (nur für Entwicklung)
  • Keine Verschlüsselung der Passwörter in DB
  • Kein Rate Limiting

Empfohlene Erweiterungen für Produktion

// 1. JWT-Authentifizierung
const jwt = require('jsonwebtoken');

app.use('/api', authenticateToken);

function authenticateToken(req, res, next) {
    const token = req.headers['authorization'];
    if (!token) return res.sendStatus(401);

    jwt.verify(token, SECRET_KEY, (err, user) => {
        if (err) return res.sendStatus(403);
        req.user = user;
        next();
    });
}

// 2. Passwort-Hashing
const bcrypt = require('bcrypt');

async function hashPassword(password) {
    return await bcrypt.hash(password, 10);
}

// 3. Rate Limiting
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 100
});

app.use('/api', limiter);

// 4. Input Validation
const { body, validationResult } = require('express-validator');

app.post('/api/inverters',
    body('name').trim().notEmpty(),
    body('ip_address').isIP(),
    (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
        }
        // ...
    }
);

Erweiterbarkeit

Plugin-System für neue Hersteller

// src/services/inverters/base.ts
export abstract class InverterBase {
    abstract connect(): Promise<void>;
    abstract readData(): Promise<InverterData>;
    abstract disconnect(): Promise<void>;
}

// src/services/inverters/custom.ts
export class CustomInverter extends InverterBase {
    async connect() {
        // Custom implementation
    }
}

// Factory Pattern
class InverterFactory {
    static create(manufacturer: string, config: any) {
        switch(manufacturer) {
            case 'SMA': return new SMAInverter(config);
            case 'KOSTAL': return new KOSTALInverter(config);
            case 'CUSTOM': return new CustomInverter(config);
            default: throw new Error('Unknown manufacturer');
        }
    }
}

Event-basierte Architektur

const EventEmitter = require('events');

class DataCollector extends EventEmitter {
    async pollInverter(inverter) {
        try {
            const data = await this.readInverterData(inverter);
            this.emit('data', { inverterId: inverter.id, data });
        } catch (error) {
            this.emit('error', { inverterId: inverter.id, error });
        }
    }
}

const collector = new DataCollector();

collector.on('data', (event) => {
    db.saveProductionData(event.inverterId, event.data);
});

collector.on('error', (event) => {
    db.logError(event.inverterId, event.error);
});

Performance-Optimierung

Caching-Strategie

const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 });

app.get('/api/inverters', (req, res) => {
    const cacheKey = 'all_inverters';
    const cached = cache.get(cacheKey);

    if (cached) {
        return res.json(cached);
    }

    const data = db.getAllInverters();
    cache.set(cacheKey, data);
    res.json(data);
});

Connection Pooling

// Für größere Deployments
const Pool = require('better-sqlite3-pool');

const pool = new Pool({
    min: 2,
    max: 10,
    idleTimeoutMillis: 30000
});

Monitoring & Logging

const winston = require('winston');

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
        new winston.transports.File({ filename: 'logs/combined.log' })
    ]
});

// Verwendung
app.post('/api/inverters', (req, res) => {
    logger.info('Creating new inverter', { ip: req.body.ip_address });
    // ...
});

Deployment-Architektur

Entwicklung

localhost:3000 (Backend)
localhost:3001 (Scanner)
localhost:3002 (Frontend)

Produktion

nginx (Reverse Proxy)
    ├─► PM2: Backend Service
    ├─► PM2: Scanner Service
    └─► Static Files: React Build

SSL/TLS (Let's Encrypt)
Firewall Rules
Monitoring (PM2, Prometheus)
Backup Strategy

Zusammenfassung

Das System ist modular aufgebaut und ermöglicht:

  • ✅ Einfache Erweiterung mit neuen Herstellern
  • ✅ Klare Trennung der Verantwortlichkeiten
  • ✅ Skalierbarkeit durch Service-Architektur
  • ✅ Wartbarkeit durch typsicheren Code
  • ✅ Testbarkeit durch lose Kopplung

Nächste Schritte für Produktion: 1. Authentifizierung implementieren 2. Passwort-Verschlüsselung hinzufügen 3. Rate Limiting aktivieren 4. Logging-System einrichten 5. Monitoring aufsetzen 6. Backup-Strategie implementieren