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
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