Skip to content

Instantly share code, notes, and snippets.

@jscmidt
Last active September 6, 2023 18:22
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jscmidt/74edf5172109721ee5931187f63c1a70 to your computer and use it in GitHub Desktop.
Save jscmidt/74edf5172109721ee5931187f63c1a70 to your computer and use it in GitHub Desktop.
iOS-widget for openWB using the Scriptable-app.
iOS-widget for openWB using the Scriptable-app.
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: yellow; icon-glyph: solar-panel;
//--------------------------------------------------------------------------------------
// Folgende Werte MÜSSEN angepasst werden:
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// IP-Adresse der openWB (von hier holt das Widget die benötigten Daten):
let oWBip = "192.168.1.1";
//--------------------------------------------------------------------------------------
// URL, auf die beim Tippen auf das Widget weitergeleitet werden soll (z.B. Home-Website von oWB):
let widgetURL = "http://192.168.1.1/openWB/web/index.php";
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Folgende Werte KÖNNEN angepasst werden:
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Im folgenden kann die Belegung der einzelnen Felder frei festgelegt werden:
// 0: Platzhalter (leeres Feld ohne Inhalt und Rahmen)
// 1: PV
// 2: Netz
// 3: Haus
// 4: Speicher SoC
// 5: Speicherleistung
// 6: Timestamp
// 7: LP1 SoC
// 8: LP2 SoC
// 9: LP1 Leistung
// 10: LP2 Leistung
// 11: LP1 und LP2 Leistung (summiert)
// 12: LP1 oder Speicherleistung (LP1 wenn geladen wird, sonst Speicher)
// 13: LP2 oder Speicherleistung (LP2 wenn geladen wird, sonst Speicher)
// 14: LP1 und LP2 oder Speicherleistung (LP1 und LP2 (summiert) wenn geladen wird, sonst Speicher)
// 15: LP1 letzte Lademenge
// 16: LP2 letzte Lademenge
// 17: LP1 und LP2 letzte Lademenge (summiert)
// 18: LP1 Lademenge diesen Monat
// 19: LP2 Lademenge diesen Monat
// 20: LP1 und LP2 Lademenge diesen Monat (summiert)
// 21: Awattar-Strompreis
// 22: Lademodus
// 23: Leistung Smarthome-Verbraucher 1
// 24: Leistung Smarthome-Verbraucher 2
// 25: Leistung Smarthome 2.0 Gerät 1
// 26: Leistung Smarthome 2.0 Gerät 2
// 27: Leistung Smarthome 2.0 Gerät 3
// 28: Leistung Smarthome 2.0 Gerät 4
// 29: Leistung Smarthome 2.0 Gerät 5
// 30: Leistung Smarthome 2.0 Gerät 6
// 31: Leistung Smarthome 2.0 Gerät 7
// 32: Leistung Smarthome 2.0 Gerät 8
// 33: Leistung Smarthome 2.0 Gerät 9
// 34: Leistung Smarthome-Verbraucher 1 + 2
// 35: Leistung Smarthome 2.0 Geräte 1 - 9
// 36: Leistung Smarthome-Verbraucher 1 + 2 und Smarthome 2.0 Geräte 1 - 9
let panel1 = 1;
let panel2 = 2;
let panel3 = 3;
let panel4 = 4;
let panel5 = 5;
let panel6 = 6;
let panel7 = 0;
let panel8 = 0;
let panel9 = 0;
let panel10 = 0;
let panel11 = 0;
let panel12 = 0;
let panel13 = 0;
let panel14 = 0;
let panel15 = 0;
//--------------------------------------------------------------------------------------
// Großes Widget:
// ///////////////////////
// // 1 // 2 // 3 //
// ///////////////////////
// // 4 // 5 // 6 //
// ///////////////////////
// // 7 // 8 // 9 //
// ///////////////////////
// // 10 // 11 // 12 //
// ///////////////////////
// // 13 // 14 // 15 //
// ///////////////////////
//--------------------------------------------------------------------------------------
// Mittleres Widget:
// ///////////////////////
// // 1 // 2 // 3 //
// ///////////////////////
// // 4 // 5 // 6 //
// ///////////////////////
//--------------------------------------------------------------------------------------
// Kleines Widget:
// /////////
// // 1 //
// /////////
// // 2 //
// /////////
//--------------------------------------------------------------------------------------
// Damit die Werte richtig skaliert werden muss die Größe des Widgets festgelegt werden:
// 0: klein, 1: mittel, 2: groß
let widgetSize = 1;
//--------------------------------------------------------------------------------------
// Soll beim Anzeigen der Ladeleistung eines LPs (auch im Wechel mit anderen Werten) der Lademodus als Footnote angezeigt werden?
let lademodusFootnote = 1;
//--------------------------------------------------------------------------------------
// Sollen kleine Graphen als Hintergrund angezeigt werden? 0: Nein, 1: Ja
let backgroundCharts = 1;
//--------------------------------------------------------------------------------------
// Sollen die einzelnen Werte und deren Graphen einen Rahmen haben?
// 0: Nein, 1-4: Breite der Rahmen
let panelBorder = 3;
//--------------------------------------------------------------------------------------
// Wie sollen Namen, Footnotes und die einzelnen Werte in ihren Bereichen angeordnet sein?
// 0: Linksbündig, 1: zentriert
let contentAlignment = 1;
//--------------------------------------------------------------------------------------
// Wie sollen Titel und Logo angeordnet sein?
// 0: Linksbündig, 1: zentriert
let titleAlignment = 1;
//--------------------------------------------------------------------------------------
// Soll im Widget-Titel das openWB-Logo angezeigt werden? 0: Nein, 1: Ja
// Beim kleinen Widget ersetzt das Logo den Titel, anstatt diesen daneben anzuzeigen
let logo = 1;
//--------------------------------------------------------------------------------------
// Titel des Widgets (steht hinter Logo wenn dieses angezeigt wird). Kein Titel: ""
let widgetTitle = "";
//--------------------------------------------------------------------------------------
// Soll der Timestamp im Titel angeigt werden? (Alternativ kann auch ein Feld mit demn Timestamp belegt werden)
// Geht nur dann, wenn entweder das mittlere oder das große Widget verwendet wird
let timestampTitle = 1;
//--------------------------------------------------------------------------------------
// Die maximale Zeit, die für das Holen der Daten verwendet wird. Bei Fehlern kann diese Zeit erhöht werden:
let timeOut = 2.0;
//--------------------------------------------------------------------------------------
// Soll bei einem Fehler (z.B. Zugriff auf das Widget aus dem Mobilfunk) statt dem Fehler
// ein Bild angezeigt werden? 0: Nein, 1: Ja
let imageAtError = 0;
//--------------------------------------------------------------------------------------
// Name des unter File-Bookmarks hinterlegten Bildes, das bei einem Error angezeigt werden soll:
let errorImageName = "IMG_4832.jpg";
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Konfiguration der Tresholds für die Farben der verschiedenen Daten
// SoC (Auto und Speicher) Tresholds sind festgelegt auf 0-30%: rot, 30-70%: gelb, 70-100%: grün
// Speicher Tresholds sind festgelegt auf Laden: Grün, Entladen: Rot, keine Ladung/Entladung: Gelb
// Netz Tresholds sind festgelegt auf Einspeisung: Grün, Bezug: Rot, keine Einspeisung/Bezug: Gelb
// Lademodus Tresholds sind festgelegt auf Sofort: Grün, Nur PV/min PV: Gelb, Standby/Stop: Rot
// Timestamp und Ladeleistung und Awattar sind immer die Standard-Schriftfarbe (siehe unten)
// Im folgenden können die Tresholds für Hausverbrauch und PV festgelegt werden:
//--------------------------------------------------------------------------------------
// Treshold 1 für PV (über diesem Wert Gelb - darunter Rot), muss kleiner als Treshold2 sein!:
let pvTresh1 = 100;
//--------------------------------------------------------------------------------------
// Treshold 2 für PV (über diesem Wert Grün - darunter Gelb), muss größer als Treshold1 sein!:
let pvTresh2 = 5000;
//--------------------------------------------------------------------------------------
// Treshold 1 für Hausverbrauch (über diesem Wert Gelb - darunter Grün), muss kleiner als Treshold2 sein!:
let hausTresh1 = 400;
//--------------------------------------------------------------------------------------
// Treshold 2 für Hausverbrauch (über diesem Wert Rot - darunter Gelb), muss größer als Treshold1 sein!:
let hausTresh2 = 1000;
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Im folgenden können die Farben des Widgets angepasst werden:
// z.B. weis: "FFFFFF", schwarz: "000000"; Eine gute Website dafür: https://htmlcolorcodes.com/
//--------------------------------------------------------------------------------------
// Standart-Schriftfarbe, für Überschrift, Footnotes usw.:
let textColor = "FFFFFF";
//--------------------------------------------------------------------------------------
// Art des Widget-Hintergrunds, 0: einfarbig, 1: Farbverlauf
let backgroundType = 1;
//--------------------------------------------------------------------------------------
// Farbe bei einfarbigem Hintergrund:
let backgroundColor = "000000";
//--------------------------------------------------------------------------------------
// Verlauffarben bei Farbverlauf als Hintergrund:
let backgroundGradient1 = "000000";
let backgroundGradient2 = "202020";
//--------------------------------------------------------------------------------------
// Farben der Hintergrundgraphen:
let chartsColorPV = "F7DC6F";
let chartsColorNetz = "D7DBDD";
let chartsColorHaus = "7DCEA0";
let chartsColorSpeicher = "85C1E9"; // Leistung und SoC
let chartsColorAwattar = "85C1E9";
let chartsColorLP1 = "FFFFFF"; // Leistung und SoC
let chartsColorLP2 = "FFFFFF"; // Leistung und SoC
let chartsColorBothLP = "FFFFFF"; // Leistung und SoC, wenn LP1 und LP2 summiert wird
let chartsColorSmarthome = "FFFFFF"; // wird nur von Smarthome 2.0 Geräten unterstützt
// Wie blass oder stark sollen die Graphen sein? (Wert zwischen 0.1 und 1.0)
let chartsThickness = 0.4;
//--------------------------------------------------------------------------------------
// Farben der Rahmen der einzelnen Werte:
let borderColorPV = "F7DC6F";
let borderColorNetz = "D7DBDD";
let borderColorHaus = "7DCEA0";
let borderColorSpeicher = "85C1E9"; // Leistung und SoC
let borderColorAwattar = "FFFFFF";
let borderColorLP1 = "FFFFFF"; // Leistung, SoC, letzte Lademenge und Monatslademenge
let borderColorLP2 = "FFFFFF"; // Leistung, SoC, letzte Lademenge und Monatslademenge
let borderColorBothLP = "FFFFFF"; // Leistung, SoC, letzte Lademenge und Monatslademenge, wenn LP1 und LP2 summiert wird
let borderColorSmarthome = "FFFFFF";
let borderColorModus = "FFFFFF";
let borderColorTimestamp = "FFFFFF";
// Wie blass oder stark sollen die Rahmen sein? (Wert zwischen 0.1 und 1.0)
let borderThickness = 0.6;
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Graph von https://kevinkub.de/
class LineChart {
constructor(width, height, values) {
this.ctx = new DrawContext();
this.ctx.size = new Size(width, height);
this.values = values;
}
_calculatePath() {
let maxValue = Math.max(...this.values);
let minValue = Math.min(...this.values);
let difference = maxValue - minValue;
let count = this.values.length;
let step = this.ctx.size.width / (count - 1);
let points = this.values.map((current, index, all) => {
let x = step*index;
let y = this.ctx.size.height - (current - minValue) / difference * this.ctx.size.height;
return new Point(x, y);
});
return this._getSmoothPath(points);
}
_getSmoothPath(points) {
let path = new Path();
path.move(new Point(0, this.ctx.size.height));
path.addLine(points[0]);
for(let i = 0; i < points.length-1; i++) {
let xAvg = (points[i].x + points[i+1].x) / 2;
let yAvg = (points[i].y + points[i+1].y) / 2;
let avg = new Point(xAvg, yAvg);
let cp1 = new Point((xAvg + points[i].x) / 2, points[i].y);
let next = new Point(points[i+1].x, points[i+1].y);
let cp2 = new Point((xAvg + points[i+1].x) / 2, points[i+1].y);
path.addQuadCurve(avg, cp1);
path.addQuadCurve(next, cp2);
}
path.addLine(new Point(this.ctx.size.width, this.ctx.size.height));
path.closeSubpath();
return path;
}
configure(fn) {
let path = this._calculatePath();
if(fn) {
fn(this.ctx, path);
} else {
this.ctx.addPath(path);
this.ctx.fillPath(path);
}
return this.ctx;
}
}
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Allgemeine Widget-Einstellungen
let widget = new ListWidget();
widget.url = widgetURL;
widget.setPadding(20, 20, 20, 20);
let nextRefresh = Date.now() + 1000*30;
widget.refreshAfterDate = new Date(nextRefresh);
// vStackV ist vertikal, vStack0 ist ist Überschrift, vStack1 ist Zeile 1, vStack2 ist Zeile 2
let vStackV = widget.addStack();
vStackV.layoutVertically();
vStackV.centerAlignContent();
let vStack0 = vStackV.addStack();
vStack0.layoutHorizontally();
vStackV.addSpacer();
let vStack1 = vStackV.addStack();
vStack1.layoutHorizontally();
vStackV.addSpacer();
let vStack2 = vStackV.addStack();
vStack2.layoutHorizontally();
let vStack3;
let vStack4;
if(widgetSize == 2){
vStackV.addSpacer();
vStack3 = vStackV.addStack();
vStack3.layoutHorizontally();
vStackV.addSpacer();
vStack4 = vStackV.addStack();
vStack4.layoutHorizontally();
vStackV.addSpacer();
vStack5 = vStackV.addStack();
vStack5.layoutHorizontally();
}
// aktuellen Timestamp erstellen und formatieren
let date = new Date();
let df = new DateFormatter();
df.dateFormat = "HH:mm";
let timestamp = (df.string(date));
// Überprüfen ob Monats-CSV benötigt wird
let monthlyUsed = false;
for(let i=1; i<16; i++){
let comparePanel = eval("panel" + i.toString());
if(comparePanel == 18 || comparePanel == 19 || comparePanel == 20){
monthlyUsed = true;
}
}
// Historiedaten für Graphen holen
let error = 0;
let csv;
let data;
let csvMonthly;
let dataMonthly;
if(backgroundCharts == 1){
csv = await getCSV();
if(error == 0){
data = csvJSON(csv, ["Zeit", "Bezug", "Einspeisung", "PV", "LP1", "LP2", "LP3", "LPGesamt", "Laden", "Entladen", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "SoC", "SoC1", "SoC2", "24", "25", "26", "Smarthome1", "Smarthome2", "Smarthome3", "Smarthome4", "Smarthome5", "Smarthome6", "Smarthome7", "Smarthome8", "Smarthome9", "36", "37", "38", "39"]);
}
if(monthlyUsed == true){
csvMonthly = await csvRequest("monthly", new Date());
if(error == 0){
dataMonthly = csvJSON(csvMonthly, ["1", "2", "3", "4", "LP1", "LP2", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29"]);
}
}
}
await setTitle();
// Inhalt des Widgets
if(error == 0){
setBackground();
vStack1.addSpacer();
await setField(vStack1, panel1);
if(widgetSize != 0){
vStack1.addSpacer();
await setField(vStack1, panel2);
vStack1.addSpacer();
await setField(vStack1, panel3);
}
vStack1.addSpacer();
vStack2.addSpacer();
if(widgetSize == 0){
await setField(vStack2, panel2);
}
else{
await setField(vStack2, panel4);
vStack2.addSpacer();
await setField(vStack2, panel5);
vStack2.addSpacer();
await setField(vStack2, panel6);
}
vStack2.addSpacer();
if(widgetSize == 2){
vStack3.addSpacer();
await setField(vStack3, panel7);
vStack3.addSpacer();
await setField(vStack3, panel8);
vStack3.addSpacer();
await setField(vStack3, panel9);
vStack3.addSpacer();
vStack4.addSpacer();
await setField(vStack4, panel10);
vStack4.addSpacer();
await setField(vStack4, panel11);
vStack4.addSpacer();
await setField(vStack4, panel12);
vStack4.addSpacer();
vStack5.addSpacer();
await setField(vStack5, panel13);
vStack5.addSpacer();
await setField(vStack5, panel14);
vStack5.addSpacer();
await setField(vStack5, panel15);
vStack5.addSpacer();
}
}
if(error != 0){
// Wenn ein Fehler aufgetreten ist
if(imageAtError == 0){
// Fehlermeldung anzeigen
addDataView(vStack1, "Ein Fehler ist aufgetreten!", "red", "Ist openWB auf der IP " + oWBip + " erreichbar?", " ", 0);
addDataView(vStack2, timestamp + " Uhr", "", "Timestamp", " ", 0);
}
else{
// Bild anzeigen
let fm = FileManager.local();
let path = fm.bookmarkedPath(errorImageName);
let errorImage = fm.readImage(path);
widget.backgroundImage = errorImage;
}
}
Script.setWidget(widget);
Script.complete();
// Größe der Widget-Vorschau
switch(widgetSize){
case 0: widget.presentSmall(); break;
case 1: widget.presentMedium(); break;
case 2: widget.presentLarge(); break;
}
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
async function setField(widget, panel){
let LP1leistung = await getData("llaktuell")*1;
let LP2leistung = await getData("llaktuells1")*1;
switch(panel){
case 12:
if(LP1leistung != 0){
panel = 9;
}
else{
panel = 5;
}
break;
case 13:
if(LP2leistung != 0){
panel = 10;
}
else{
panel = 5;
}
break;
case 14:
if(LP1leistung + LP2leistung != 0){
panel = 11;
}
else{
panel = 5;
}
break;
}
await setFieldFinal(widget, panel, LP1leistung, LP2leistung);
}
async function setFieldFinal(widget, panel, LP1leistung, LP2leistung){
let wert;
let value;
let valueColor;
let name;
let footnote;
let borderColor;
let chart;
let historicalsBezug;
let historicalsEinspeisung;
let historicalsLaden;
let historicalsEntladen;
let historicalsPV;
let historicalsLP1;
let historicalsLP2;
let border = panelBorder;
switch(panel){
case 0:
//Platzhalter
value = " ";
name = " ";
footnote = " ";
border = 0;
borderColor = "FFFFFF";
chart = 0;
break;
case 1:
// PV
historicals = getCalc(data, "PV");
wert = (await getData("pvallwatt")*-1); // Wert ist negativ, deswegen *-1
value = wert.toString() + "W";
valueColor = defineColor(pvTresh1, pvTresh2, wert);
name = "PV";
footnote = " ";
borderColor = borderColorPV;
chart = createChart(historicals, chartsColorPV);
break;
case 2:
// Netz
historicalsBezug = getCalc(data, "Bezug");
historicalsEinspeisung = getCalc(data, "Einspeisung");
historicals = [];
for(let i=0; i<12; i++){
historicals[i] = historicalsBezug[i] - historicalsEinspeisung[i];
}
wert = (await getData("wattbezug")*1); // Einspeissung ist negativ
value = wert.toString() + "W";
valueColor = defineColorNegative(-100, 100, wert);
name = "Netz";
footnote = defineFootnote(100, -100, "Bezug", "Einspeisen", "Speicherregelung", wert);
borderColor = borderColorNetz;
chart = createChart(historicals, chartsColorNetz);
break;
case 3:
// Haus
historicalsBezug = getCalc(data, "Bezug");
historicalsEinspeisung = getCalc(data, "Einspeisung");
historicalsLaden = getCalc(data, "Laden");
historicalsEntladen = getCalc(data, "Entladen");
historicalsPV = getCalc(data, "PV");
// Hausverbrauch = Netzbezug + Speicherentladung + PV-Erzeugung - Netzeinspeisung - Speicherladung
historicals = [];
for(let i=0; i<12; i++){
historicals[i] = historicalsBezug[i] + historicalsEntladen[i] + historicalsPV[i] - historicalsEinspeisung[i] - historicalsLaden[i];
}
wert = (await getData("hausverbrauch")*1);
value = wert.toString() + "W";
valueColor = defineColorNegative(hausTresh1, hausTresh2, wert);
name = "Haus";
footnote = " ";
borderColor = borderColorHaus;
chart = createChart(historicals, chartsColorHaus);
break;
case 4:
// Speicher SoC
historicals = getCalc(data, "SoC");
wert = (await getData("speichersoc")*1);
value = wert.toString() + "%";
valueColor = defineColor(30, 70, wert);
name = "SoC";
footnote = " ";
borderColor = borderColorSpeicher;
chart = createChart(historicals, chartsColorSpeicher);
break;
case 5:
// Speicherleistung
historicalsLaden = getCalc(data, "Laden");
historicalsEntladen = getCalc(data, "Entladen");
historicals = [];
for(let i=0; i<12; i++){
historicals[i] = historicalsLaden[i] - historicalsEntladen[i];
}
wert = (await getData("speicherleistung")*1); // Entladen ist negativ
value = wert.toString() + "W";
valueColor = defineColor(-50, 50, wert)
name = "Speicher";
footnote = defineFootnote(50, -50, "Laden", "Entladen", "Netzregelung", wert);
borderColor = borderColorSpeicher;
chart = createChart(historicals, chartsColorSpeicher);
break;
case 6:
// Timestamp
value = timestamp;
valueColor = "";
name = "Timestamp";
footnote = " ";
borderColor = borderColorTimestamp;
chart = 0;
break;
case 7:
// LP1 SoC
historicals = getCalc(data, "SoC1");
wert = (await getData("soc")*1); // SoC von LP1;
value = wert.toString() + "%"
valueColor = defineColor(30, 70, wert);
name = "LP1 SoC";
footnote = " ";
borderColor = borderColorLP1;
chart = createChart(historicals, chartsColorLP1);
break;
case 8:
// LP2 SoC
historicals = getCalc(data, "SoC2");
wert = (await getData("soc1")*1); // SoC von LP2;
value = wert.toString() + "%";
valueColor = defineColor(30, 70, wert);
name = "LP2 SoC";
footnote = " ";
borderColor = borderColorLP2;
chart = createChart(historicals, chartsColorLP2);
break;
case 9:
// LP1 Leistung
historicals = getCalc(data, "LP1");
wert = LP1leistung; // Leistung von LP1;
value = wert.toString() + "W";
valueColor = "";
name = "LP1 Leist.";
footnote = " ";
borderColor = borderColorLP1;
chart = createChart(historicals, chartsColorLP1);
break;
case 10:
// LP2 Leistung
historicals = getCalc(data, "LP2");
wert = LP2leistung; // Leistung von LP2;
value = wert.toString() + "W";
valueColor = "";
name = "LP2 Leist.";
footnote = " ";
borderColor = borderColorLP2;
chart = createChart(historicals, chartsColorLP2);
break;
case 11:
// LP1 und LP2 Leistung (summiert)
historicalsLP1 = getCalc(data, "LP1");
historicalsLP2 = getCalc(data, "LP2");
historicals = [];
for(let i=0; i<12; i++){
historicals[i] = historicalsLP1[i] + historicalsLP2[i];
}
wert = LP1leistung + LP2leistung; // Leistung von LP1 und LP2;
value = wert.toString() + "W";
valueColor = "";
name = "LP1+2 Leist.";
footnote = " ";
borderColor = borderColorBothLP;
chart = createChart(historicals, chartsColorBothLP);
break;
case 15:
// LP1 letzte Lademenge
wert = (await getData("aktgeladen")*1);
value = wert.toString() + "kWh";
valueColor = "";
name = "LP1 zuletzt";
footnote = " ";
borderColor = borderColorLP1;
chart = 0;
break;
case 16:
// LP2 letzte Lademenge
wert = (await getData("aktgeladens1")*1);
value = wert.toString() + "kWh";
valueColor = "";
name = "LP2 zuletzt";
footnote = " ";
borderColor = borderColorLP2;
chart = 0;
break;
case 17:
// LP1 und LP2 letzte Lademenge (summiert)
wert = (await getData("aktgeladen")*1) + (await getData("aktgeladens1")*1);
value = wert.toString() + "kWh";
valueColor = "";
name = "LP1+2 zuletzt";
footnote = " ";
borderColor = borderColorBothLP;
chart = 0;
break;
case 18:
// LP1 Monatslademenge
wert = (Number(dataMonthly[dataMonthly.length-2]["LP1"]) - Number(dataMonthly[0]["LP1"]))/1000;
value = wert.toString() + "kWh";
valueColor = "";
name = "LP1 Monat";
footnote = " ";
borderColor = borderColorLP1;
chart = 0;
break;
case 19:
// LP2 Monatslademenge
wert = (Number(dataMonthly[dataMonthly.length-2]["LP2"]) - Number(dataMonthly[0]["LP2"]))/1000;
value = wert.toString() + "kWh";
valueColor = "";
name = "LP2 Monat";
footnote = " ";
borderColor = borderColorLP2;
chart = 0;
break;
case 20:
// LP1 und LP2 Monatslademenge (summiert)
wert = ((Number(dataMonthly[dataMonthly.length-2]["LP1"]) - Number(dataMonthly[0]["LP1"])) + (Number(dataMonthly[dataMonthly.length-2]["LP2"]) - Number(dataMonthly[0]["LP2"])))/1000;
value = wert.toString() + "kWh";
valueColor = "";
name = "LP1+2 Monat";
footnote = " ";
borderColor = borderColorBothLP;
chart = 0;
break;
case 21:
// Awattar-Strompreis
wert = (await getData("etproviderprice")*1); // aktueller Strompreis
value = wert.toString() + "ct";
valueColor = defineColorNegative(1, 5, wert);
name = "Strompreis";
// Zeitstempel für Footnote erzeugen
let hoursDate = new Date();
let hoursDate2 = new Date();
hoursDate2.setHours(hoursDate2.getHours() + 1)
let hoursDF = new DateFormatter();
hoursDF.dateFormat = "HH";
let hours = (hoursDF.string(hoursDate));
let hours2 = (hoursDF.string(hoursDate2));
footnote = hours + " - " + hours2 + " Uhr";
borderColor = borderColorAwattar;
// Holen und Aufbereiten der Awattar-Graphdaten
awattarRaw = await getData("etprovidergraphlist");
let lines = awattarRaw.split("\n");
let historicalsAwattar = [];
for(let i = 0; i < 10; i++){
let seperated = lines[i].split(",");
historicalsAwattar[i] = Number(seperated[1]);
}
chart = createChart(historicalsAwattar, chartsColorAwattar);
break;
case 22:
// Lademodus
wert = (await getData("lademodus")*1); // Lademodus
value = wert.toString();
valueColor = LademodusColor(wert);
name = "Lademodus";
footnote = LademodusText(wert);
borderColor = borderColorModus;
chart = 0;
break;
// Smarthome-Geräte
case 23:
wert = (await getData("verbraucher1_watt")*1);
name = "Verbr. 1";
footnote = (await getData("verbraucher1_name")).trim();
chart = 0;
break;
case 24:
wert = (await getData("verbraucher2_watt")*1);
name = "Verbr. 2";
footnote = (await getData("verbraucher2_name")).trim();
chart = 0;
break;
case 25:
historicals = getCalc(data, "Smarthome1");
wert = (await getData("smarthome_device_1wh0")*1);
name = "Gerät 1";
footnote = " ";
chart = createChart(historicals, chartsColorSmarthome);
break;
case 26:
historicals = getCalc(data, "Smarthome2");
wert = (await getData("smarthome_device_2wh0")*1);
name = "Gerät 2";
footnote = " ";
chart = createChart(historicals, chartsColorSmarthome);
break;
case 27:
historicals = getCalc(data, "Smarthome3");
wert = (await getData("smarthome_device_3wh0")*1);
name = "Gerät 3";
footnote = " ";
chart = createChart(historicals, chartsColorSmarthome);
break;
case 28:
historicals = getCalc(data, "Smarthome4");
wert = (await getData("smarthome_device_4wh0")*1);
name = "Gerät 4";
footnote = " ";
chart = createChart(historicals, chartsColorSmarthome);
break;
case 29:
historicals = getCalc(data, "Smarthome5");
wert = (await getData("smarthome_device_5wh0")*1);
name = "Gerät 5";
footnote = " ";
chart = createChart(historicals, chartsColorSmarthome);
break;
case 30:
historicals = getCalc(data, "Smarthome6");
wert = (await getData("smarthome_device_6wh0")*1);
name = "Gerät 6";
footnote = " ";
chart = createChart(historicals, chartsColorSmarthome);
break;
case 31:
historicals = getCalc(data, "Smarthome7");
wert = (await getData("smarthome_device_7wh0")*1);
name = "Gerät 7";
footnote = " ";
chart = createChart(historicals, chartsColorSmarthome);
break;
case 32:
historicals = getCalc(data, "Smarthome8");
wert = (await getData("smarthome_device_8wh0")*1);
name = "Gerät 8";
footnote = " ";
chart = createChart(historicals, chartsColorSmarthome);
break;
case 33:
historicals = getCalc(data, "Smarthome9");
wert = (await getData("smarthome_device_9wh0")*1);
name = "Gerät 9";
footnote = " ";
chart = createChart(historicals, chartsColorSmarthome);
break;
// Leistung Smarthome-Verbraucher 1 + 2
case 34:
wert = (await getData("verbraucher1_watt")*1) + (await getData("verbraucher2_watt")*1);
name = "Verbraucher";
footnote = "1 + 2";
chart = 0;
break;
// Leistung Smarthome 2.0 Geräte 1 - 9
case 35:
wert = 0;
for(let i=1; i<10; i++){
let newWert = (await getData("smarthome_device_" + i.toString() + "wh0")*1);
let newHistoricals = getCalc(data, "Smarthome" + i.toString());
for(let i=0; i<12; i++){
historicals[i] += newHistoricals[i];
}
if(newWert){
wert += newWert;
}
}
name = "Geräte";
footnote = "1 - 9";
chart = createChart(historicals, chartsColorSmarthome);
break;
// Leistung Smarthome-Verbraucher 1 + 2 und Smarthome 2.0 Geräte 1 - 9
case 36:
wert = (await getData("verbraucher1_watt")*1) + (await getData("verbraucher2_watt")*1);
for(let i=1; i<10; i++){
let newWert = (await getData("smarthome_device_" + i.toString() + "wh0")*1);
if(newWert){
wert += newWert;
}
}
name = "Smarthome";
footnote = "Verbr. + Geräte";
chart = 0;
break;
// Zuordnung nicht erfolgreich
default:
error = 4;
break;
}
//Zusatzinformationen für alle Smarthome-Geräte
if(panel >= 23 && panel <= 36){
value = wert.toString() + "W";
valueColor = defineColor(1, 11, wert);
borderColor = borderColorSmarthome;
}
if((panel == 9 || panel == 10 || panel == 11) && lademodusFootnote == 1){
footnote = LademodusText(await getData("lademodus")*1);
}
if(error == 0){
addDataView(widget, value, valueColor, name, footnote, borderColor, chart, border);
}
}
function LademodusText(modus){
switch(modus){
case 0:
footnote = "Sofortladen";
break;
case 1:
footnote = "Min & PV";
break;
case 2:
footnote = "Nur PV";
break;
case 3:
footnote = "Stop";
break;
case 4:
footnote = "Standby";
break;
default:
footnote = " ";
break;
}
return(footnote);
}
function LademodusColor(modus){
switch(modus){
case 0:
color = "green";
break;
case 1:
color = "yellow";
break;
case 2:
color = "yellow";
break;
case 3:
color = "red";
break;
case 4:
color = "red";
break;
default:
color = " ";
break;
}
return(color);
}
function setBackground(){
// Hintergrundfarbe oder Verlauf festlegen
if(backgroundType == 0){
widget.backgroundColor = new Color(backgroundColor);
}
else{
const gradient = new LinearGradient()
gradient.locations = [0, 1]
gradient.colors = [ new Color(backgroundGradient1), new Color(backgroundGradient2) ]
widget.backgroundGradient = gradient
}
}
// Erstellt den Widget-Titel
async function setTitle(){
if(titleAlignment == 1){
vStack0.addSpacer();
}
else{
vStack0.addSpacer(10);
}
if(titleAlignment == 1 && error == 0 && widgetSize != 0){
if(timestampTitle == 0){
if(widgetTitle && logo){
vStack0.addSpacer(36);
}
else{
vStack0.addSpacer(32);
}
}
else{
if(widgetTitle && logo){
vStack0.addSpacer(70);
}
else{
vStack0.addSpacer(66);
}
}
}
// Logo holen und darstellen
if(logo == 1 && error == 0){
img = vStack0.addImage(await setLogo());
img.imageSize = new Size(50,20);
}
// Überschrift des Widgets
if((imageAtError!= 1 || error == 0) && ((widgetSize == 0 && logo == 1)!= 1) && widgetTitle){
if(logo == 1 && error == 0){
vStack0.addSpacer(10);
}
let header = vStack0.addText(widgetTitle);
header.font = Font.mediumSystemFont(12);
header.textColor = new Color(textColor);
}
vStack0.addSpacer();
// Timestamp in Titel
if(timestampTitle == 1 && error == 0 && widgetSize != 0){
let ts = vStack0.addText(timestamp);
ts.font = Font.mediumSystemFont(12);
ts.textColor = new Color(textColor);
}
}
// Holt das Logo von oWB und bearbeitet dieses
async function setLogo(){
let result;
try{
let imgReq = new Request("http://" + oWBip + "/openWB/web/img/favicons/apple-icon-180x180.png");
imgReq.timeoutInterval = timeOut;
let image = await imgReq.loadImage();
dc = new DrawContext();
dc.opaque = true;
dc.size = new Size(180,56);
dc.drawImageAtPoint(image, new Point(-0, -54));
result = dc.getImage();
}
catch(err){
error = 1;
}
return(result);
}
// zentriert/linksbündigt/rechtbündigt Text
function AlignStack(stack, pos){
if(contentAlignment == 1 || (contentAlignment == 0 && pos == 1)){
stack.addSpacer();
}
}
// legt ein neues Datenstack an
function addDataView(widget, data, color, name, foot, borderColor, img, border){
let viewStack = widget.addStack();
viewStack.layoutVertically();
viewStack.cornerRadius = 5;
if(borderColor != 0){
viewStack.size = new Size(97, 47);
viewStack.borderWidth = border;
viewStack.borderColor = new Color(borderColor, borderThickness);
viewStack.setPadding(5, 5, 5, 3);
}
let labelStack = viewStack.addStack();
AlignStack(labelStack, 0);
let label = labelStack.addText(name);
label.font = Font.mediumSystemFont(12);
label.textColor = new Color(textColor);
AlignStack(labelStack, 1);
let footnoteStack = viewStack.addStack();
AlignStack(footnoteStack, 0);
let footnote = footnoteStack.addText(foot);
footnote.font = Font.mediumSystemFont(8);
footnote.textColor = new Color(textColor);
AlignStack(footnoteStack, 1);
let valueStack = viewStack.addStack();
AlignStack(valueStack, 0);
let value = valueStack.addText(data);
value.font = Font.mediumSystemFont(18);
value.textColor = colorForString(color);
AlignStack(valueStack, 1);
if(backgroundCharts == 1){
if(img != 0){
viewStack.backgroundImage = img;
}
}
}
// holt die aktuellen Daten
async function getData(file){
let result = 0;
try {
let url = "http://" + oWBip + "/openWB/ramdisk/" + file;
let req = new Request(url);
req.timeoutInterval = timeOut;
result = await req.loadString();
}
catch(err) {
result = 0;
error = 2;
}
return(result);
}
// Legt Farbe fest
function defineColor(TreshYellow, TreshGreen, value){
if (value >= TreshGreen){
return("green");
}
else if (value >= TreshYellow){
return("yellow");
}
else{
return("red");
}
}
// Legt Farbe fest
function defineColorNegative(TreshYellow, TreshRed, value){
if (value >= TreshRed){
return("red");
}
else if (value >= TreshYellow){
return("yellow");
}
else{
return("green");
}
}
// Legt Footnote fest
function defineFootnote(groesserAls, kleinerAls, groesserText, kleinerText, sonstText, value){
if (value > groesserAls){
return(groesserText);
}
else if (value < kleinerAls){
return(kleinerText);
}
else{
return(sonstText);
}
}
//Gibt das entsprechende Color-Objekt zurück
function colorForString(colorString){
if (colorString == "red") {
return Color.red();
}
if (colorString == "yellow") {
return Color.yellow();
}
if (colorString == "green") {
return Color.green();
}
return (new Color(textColor));
}
// Erstellt Graph und liefert Bild davon zurück
function createChart(data, color){
if(backgroundCharts == 1){
let chart = new LineChart(97, 47, data).configure((ctx, path) => {
ctx.opaque = false;
ctx.setFillColor(new Color(color, chartsThickness));
ctx.addPath(path);
ctx.fillPath(path);
}).getImage();
return(chart);
}
}
// Holt CSV-Daten von oWB
async function csvRequest(type, date){
let txt;
let df = new DateFormatter();
switch(type){
case "daily":
df.dateFormat = "YYYYMMdd";
break;
case "monthly":
df.dateFormat = "YYYYMM";
break;
}
let fdate = (df.string(date));
let url = ("http://" + oWBip + "/openWB/web/logging/data/" + type + "/" + fdate + ".csv");
let req = new Request(url);
req.timeoutInterval = timeOut;
try{
txt = await req.loadString();
}
catch(err){
error = 3;
}
return(txt);
}
// Zusammensetzen der CSV-Daten des aktuellen und des vorherigen Tages
async function getCSV(){
let date = new Date();
let today = await csvRequest("daily", date);
date.setDate(date.getDate()-1)
let yesterday = await csvRequest("daily", date);
let result = yesterday + today;
return(result);
}
// Kalkuliert die Differenz zwischen den Werten der letzten Stunde
function getCalc(data, key){
let result = [];
if(backgroundCharts == 1){
let resultVar = 11;
for(let i=0; i<12; i++){
if(key != "SoC" && key != "SoC1" && key != "SoC2"){
result[resultVar] = Number((data[data.length-2-i][key]) - Number(data[data.length-3-i][key]));
}
else{
result[resultVar] = Number(data[data.length-2-i][key]);
}
resultVar --;
}
}
return(result);
}
// Konvertiert CSV zu einem Objekt
function csvJSON(csv, headers){
let lines = csv.split("\n");
let result = [];
for(let i=0;i<lines.length;i++){
let obj = {};
let currentline=lines[i].split(",");
for(let j=0;j<headers.length;j++){
obj[headers[j]] = currentline[j];
}
result.push(obj);
}
return(result);
}
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: yellow; icon-glyph: solar-panel;
//--------------------------------------------------------------------------------------
// openWB-Widget, Version 6
//--------------------------------------------------------------------------------------
let config = new Object();
//--------------------------------------------------------------------------------------
// INFORMATION
//--------------------------------------------------------------------------------------
// Die Konfiguration kann für mehrere Instanzen des Widgets erfolgen.
// Die konfigurierten Instanzen können über de Parameter in den Widget-Einstellungen ausgewählt werden.
// Wird nur eine Instanz verwendet, muss der Parameter nicht festgelegt werden.
// Beispiel ein Widget: config.panel1 = [1]; ODER config.panel1 = 1;
// Beispiel mehrere Widgets: config.panel1 = [1,3,2]; --> Konfiguration von drei Widgets.
// In diesem Beispiel gilt 1 für Widgets mit dem Parameter 0, 3 für Widgets mit dem Parameter 1
// und 2 für Widgets mit dem Parameter 2
// Wenn ein Konfigurationsparameter für alle Instanzen gleich ist (z.B. oWBip),
// muss dieser nur einmal angegeben werden (wie wenn nur eine Instanz verwendet wird)
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Folgende Werte stehen ZUR VERFÜGUNG (LEGENDE):
// Die nachfolgende Nummerierung wird in der anschließenden Konfiguration mehrmals benötigt.
//--------------------------------------------------------------------------------------
// 0: Platzhalter (leeres Feld ohne Inhalt und Rahmen)
// 1: PV
// 2: Netz
// 3: Haus
// 4: Speicher SoC
// 5: Speicherleistung
// 6: Timestamp
// 7: LP1 SoC
// 8: LP2 SoC
// 9: LP1 Leistung
// 10: LP2 Leistung
// 11: LP1 und LP2 Leistung (summiert)
// 12: LP1 oder Speicherleistung (LP1 wenn geladen wird, sonst Speicher)
// 13: LP2 oder Speicherleistung (LP2 wenn geladen wird, sonst Speicher)
// 14: LP1 und LP2 oder Speicherleistung (LP1 und LP2 (summiert) wenn geladen wird, sonst Speicher)
// 15: LP1 letzte Lademenge
// 16: LP2 letzte Lademenge
// 17: LP1 und LP2 letzte Lademenge (summiert)
// 18: LP1 Lademenge diesen Monat
// 19: LP2 Lademenge diesen Monat
// 20: LP1 und LP2 Lademenge diesen Monat (summiert)
// 21: Awattar-Strompreis
// 22: Lademodus
// 23: Leistung Smarthome-Verbraucher 1
// 24: Leistung Smarthome-Verbraucher 2
// 25: Leistung Smarthome 2.0 Gerät 1
// 26: Leistung Smarthome 2.0 Gerät 2
// 27: Leistung Smarthome 2.0 Gerät 3
// 28: Leistung Smarthome 2.0 Gerät 4
// 29: Leistung Smarthome 2.0 Gerät 5
// 30: Leistung Smarthome 2.0 Gerät 6
// 31: Leistung Smarthome 2.0 Gerät 7
// 32: Leistung Smarthome 2.0 Gerät 8
// 33: Leistung Smarthome 2.0 Gerät 9
// 34: Leistung Smarthome-Verbraucher 1 + 2
// 35: Leistung Smarthome 2.0 Geräte 1 - 9
// 36: Leistung Smarthome-Verbraucher 1 + 2 und Smarthome 2.0 Geräte 1 - 9
// 37: PV Erzeugung diesen Monat
// 38: Netzeinspeisung diesen Monat
// 39: Netzbezug diesen Monat
// 40: Hausverbrauch diesen Monat
// 41: Speicherladung diesen Monat
// 42: Speicherentladung diesen Monat
// 43: Eigenverbrauch diesen Monat (PV - Netzeinspeissung)
// 44: PV Erzeugung heute
// 45: Netzeinspeisung heute
// 46: Netzbezug heute
// 47: Hausverbrauch heute
// 48: Speicherladung heute
// 49: Speicherentladung heute
// 50: Eigenverbrauch heute (PV - Netzeinspeissung)
// 51: Autarkiequote diesen Monat Haus + LP
// 52: Autarkiequote diese Monat Haus (ohne LP, mit Annahme 100% PV geladen)
// 53: Eigenverbrauchsquote diesen Monat
// 54: Autarkiequote heute Haus + LP
// 55: Autarkiequote heute Haus (ohne LP, mit Annahme 100% PV geladen)
// 56: Eigenverbrauchsquote heute
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Folgende Werte MÜSSEN angepasst werden:
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// IP-Adresse der openWB (von hier holt das Widget die benötigten Daten):
config.oWBip = ["192.168.1.1"];
//--------------------------------------------------------------------------------------
// URL, auf die beim Tippen auf das Widget weitergeleitet werden soll (z.B. Home-Website von oWB):
config.widgetURL = ["http://192.168.1.1/openWB/web/index.php"];
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Folgende Werte KÖNNEN angepasst werden:
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Im folgenden kann die Belegung der einzelnen Felder frei festgelegt werden:
// Die Nummerierung richtet sich an die Legende am Anfang der Konfiguration.
config.panel1 = [1];
config.panel2 = [2];
config.panel3 = [3];
config.panel4 = [4];
config.panel5 = [5];
config.panel6 = [6];
config.panel7 = [0];
config.panel8 = [0];
config.panel9 = [0];
config.panel10 = [0];
config.panel11 = [0];
config.panel12 = [0];
config.panel13 = [0];
config.panel14 = [0];
config.panel15 = [0];
//--------------------------------------------------------------------------------------
// Großes Widget:
// ///////////////////////
// // 1 // 2 // 3 //
// ///////////////////////
// // 4 // 5 // 6 //
// ///////////////////////
// // 7 // 8 // 9 //
// ///////////////////////
// // 10 // 11 // 12 //
// ///////////////////////
// // 13 // 14 // 15 //
// ///////////////////////
//--------------------------------------------------------------------------------------
// Mittleres Widget:
// ///////////////////////
// // 1 // 2 // 3 //
// ///////////////////////
// // 4 // 5 // 6 //
// ///////////////////////
//--------------------------------------------------------------------------------------
// Kleines Widget:
// /////////
// // 1 //
// /////////
// // 2 //
// /////////
//--------------------------------------------------------------------------------------
// Damit die Werte richtig skaliert werden muss die Größe des Widgets festgelegt werden:
// 0: klein, 1: mittel, 2: groß
config.widgetSize = [1];
//--------------------------------------------------------------------------------------
// Soll beim Anzeigen der Ladeleistung eines LPs (auch im Wechel mit anderen Werten) der Lademodus
// als Footnote angezeigt werden? 0: Nein, 1: Ja
config.lademodusFootnote = [1];
//--------------------------------------------------------------------------------------
// Sollen kleine Graphen als Hintergrund angezeigt werden? 0: Nein, 1: Ja
config.backgroundCharts = [1];
//--------------------------------------------------------------------------------------
// Sollen die einzelnen Werte und deren Graphen einen Rahmen haben?
// 0: Nein, 1-4: Breite der Rahmen
config.panelBorder = [3];
//--------------------------------------------------------------------------------------
// Wie sollen Namen, Footnotes und die einzelnen Werte in ihren Bereichen angeordnet sein?
// 0: Linksbündig, 1: zentriert
config.contentAlignment = [1];
//--------------------------------------------------------------------------------------
// Wie sollen Titel und Logo angeordnet sein?
// 0: Linksbündig, 1: zentriert
config.titleAlignment = [1];
//--------------------------------------------------------------------------------------
// Soll im Widget-Titel das openWB-Logo angezeigt werden? 0: Nein, 1: Ja
// Beim kleinen Widget ersetzt das Logo den Titel, anstatt diesen daneben anzuzeigen
config.logo = [1];
//--------------------------------------------------------------------------------------
// Titel des Widgets (steht hinter Logo wenn dieses angezeigt wird). Kein Titel: ""
config.widgetTitle = [""];
//--------------------------------------------------------------------------------------
// Soll der Timestamp im Titel angeigt werden? 0: Nein, 1: Ja
// (Alternativ kann auch ein Feld mit dem Timestamp belegt werden)
// Geht nur dann, wenn entweder das mittlere oder das große Widget verwendet wird
config.timestampTitle = [1];
//--------------------------------------------------------------------------------------
// Die maximale Zeit in Sekunden, die für das Holen der Daten verwendet wird.
// Bei Fehlern kann dieser Wert erhöht werden:
config.timeOut = [5.0];
//--------------------------------------------------------------------------------------
// Soll bei einem globalen Fehler (z.B. Zugriff auf das Widget aus dem Mobilfunk) statt dem Fehler
// ein Bild angezeigt werden? 0: Nein, 1: Ja
config.imageAtError = [0];
//--------------------------------------------------------------------------------------
// Name des unter File-Bookmarks hinterlegten Bildes, das bei einem Error angezeigt werden soll:
config.errorImageName = ["IMG_4832.jpg"];
//--------------------------------------------------------------------------------------
// Was soll bei einem Fehler in einem Feld angezeigt werden? 0: Fehler, 1: Platzhalter
config.placeholderAtError = [1];
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Im folgenden können die STATISCHEN FARBEN des Widgets angepasst werden:
// z.B. weis: "FFFFFF", schwarz: "000000"; Eine gute Website dafür: https://htmlcolorcodes.com/
//--------------------------------------------------------------------------------------
// Standart-Farbe, wird verwendet für Überschrift, Footnotes usw.
// oder wenn benutzerdefinierte Farben falsch oder nicht konfiguriert sind
config.defaultColor = ["FFFFFF"];
//--------------------------------------------------------------------------------------
// Art des Widget-Hintergrunds, 0: einfarbig, 1: Farbverlauf
config.backgroundType = [1];
//--------------------------------------------------------------------------------------
// Farbe bei einfarbigem Hintergrund:
config.backgroundColor = ["000000"];
//--------------------------------------------------------------------------------------
// Verlauffarben bei Farbverlauf als Hintergrund:
config.backgroundGradient1 = ["000000"];
config.backgroundGradient2 = ["202020"];
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Im folgenden können die DYNAMISCHEN FARBEN des Widgets angepasst werden:
// Die Nummerierung richtet sich an die Legende am Anfang der Konfiguration.
// Folgendes Schema wird verwendet: [{"Treshold1": "Farbe1", "Treshold2": "Farbe2", ...}]
// Es wird immer die Farbe des höchsten überschrittenen Tresholds verwendet
// Mögliche Werte für die Tresholds sind Zahlen (z.B. "100") oder
// Berechnungen mit variablen Daten anderer Felder (z.B. "100-$5") oder
// "else", welches verwendet wird wenn keine andere Regel zutrifft (z.B. unter kleinstem Treshold)
// Mögliche Werte für die Farben sind Farbcodes (z.B. "000000"),
// "red", "yellow", "green" und "default" (oben festgelegte Standard-Schriftfarbe)
// Bei Oder-Feldern (12/13/14) werden beide Möglichkeiten über die
// Ursprungsfelder festgelegt (5/9/10/11)
// Gesamtbeispiel: Wenn PV größer als Hausverbrauch ($3) + Puffer (100) wird PV-Wert gelb,
// wenn PV größer als Hausverbrauch ($3) + Ladeleistung ($11) + Puffer (101) wird PV-Wert grün
// Puffer bei grün 101, damit dieser Treshold höher als Gelb wenn Ladeleistung = 0
// config.textColor1 = [{"else": "red", "$3+100": "yellow", "$3+$11+101": "green"}];
//--------------------------------------------------------------------------------------
// Farben der WERTE:
// Ausgenommen ist der Timestamp (6), hier kann nur eine Farbe festgelegt werden.
config.textColor1 = [{"else": "red", "100": "yellow", "5000": "green"}];
config.textColor2 = [{"else": "green", "-100": "yellow", "100": "red"}];
config.textColor3 = [{"else": "green", "400": "yellow", "1000": "red"}];
config.textColor4 = [{"else": "red", "30": "yellow", "70": "green"}];
config.textColor5 = [{"else": "red", "-50": "yellow", "50": "green"}];
config.textColor6 = ["FFFFFF"]; //nur eine Farbe je Instanz!
config.textColor7 = [{"else": "red", "30": "yellow", "70": "green"}];
config.textColor8 = [{"else": "red", "30": "yellow", "70": "green"}];
config.textColor9 = [{"else": "red", "100": "green"}];
config.textColor10 = [{"else": "red", "100": "green"}];
config.textColor11 = [{"else": "red", "100": "green"}];
config.textColor15 = [{"else": "green", "100": "yellow", "1000": "red"}];
config.textColor16 = [{"else": "green", "100": "yellow", "1000": "red"}];
config.textColor17 = [{"else": "green", "100": "yellow", "1000": "red"}];
config.textColor18 = [{"else": "green", "100": "yellow", "1000": "red"}];
config.textColor19 = [{"else": "green", "100": "yellow", "1000": "red"}];
config.textColor20 = [{"else": "green", "100": "yellow", "1000": "red"}];
config.textColor21 = [{"else": "green", "1": "yellow", "5": "red"}];
config.textColor22 = [{"else": "green", "1": "yellow", "3": "red"}];
config.textColor23 = [{"else": "green", "500": "yellow", "1000": "red"}];
config.textColor24 = [{"else": "green", "500": "yellow", "1000": "red"}];
config.textColor25 = [{"else": "green", "500": "yellow", "1000": "red"}];
config.textColor26 = [{"else": "green", "500": "yellow", "1000": "red"}];
config.textColor27 = [{"else": "green", "500": "yellow", "1000": "red"}];
config.textColor28 = [{"else": "green", "500": "yellow", "1000": "red"}];
config.textColor29 = [{"else": "green", "500": "yellow", "1000": "red"}];
config.textColor30 = [{"else": "green", "500": "yellow", "1000": "red"}];
config.textColor31 = [{"else": "green", "500": "yellow", "1000": "red"}];
config.textColor32 = [{"else": "green", "500": "yellow", "1000": "red"}];
config.textColor33 = [{"else": "green", "500": "yellow", "1000": "red"}];
config.textColor34 = [{"else": "green", "1000": "yellow", "2000": "red"}];
config.textColor35 = [{"else": "green", "4500": "yellow", "9000": "red"}];
config.textColor36 = [{"else": "green", "5500": "yellow", "11000": "red"}];
config.textColor37 = [{"else": "red", "1000": "yellow", "2000": "green"}];
config.textColor38 = [{"else": "red", "100": "yellow", "1000": "green"}];
config.textColor39 = [{"else": "green", "10": "yellow", "100": "red"}];
config.textColor40 = [{"else": "green", "200": "yellow", "500": "red"}];
config.textColor41 = [{"else": "red", "100": "yellow", "200": "green"}];
config.textColor42 = [{"else": "red", "100": "yellow", "200": "green"}];
config.textColor43 = [{"else": "red", "100": "yellow", "500": "green"}];
config.textColor44 = [{"else": "red", "5": "yellow", "15": "green"}];
config.textColor45 = [{"else": "red", "5": "yellow", "15": "green"}];
config.textColor46 = [{"else": "green", "1": "yellow", "5": "red"}];
config.textColor47 = [{"else": "green", "1": "yellow", "5": "red"}];
config.textColor48 = [{"else": "red", "1": "yellow", "3": "green"}];
config.textColor49 = [{"else": "red", "1": "yellow", "3": "green"}];
config.textColor50 = [{"else": "red", "1": "yellow", "3": "green"}];
config.textColor51 = [{"else": "red", "50": "yellow", "80": "green"}];
config.textColor52 = [{"else": "red", "50": "yellow", "80": "green"}];
config.textColor53 = [{"else": "red", "50": "yellow", "80": "green"}];
config.textColor54 = [{"else": "red", "50": "yellow", "80": "green"}];
config.textColor55 = [{"else": "red", "50": "yellow", "80": "green"}];
config.textColor56 = [{"else": "red", "50": "yellow", "80": "green"}];
//--------------------------------------------------------------------------------------
// Farben der HINTERGRUNDGRAPHEN:
// Hintergrundgraphen werden nicht unterstützt auf den Feldern 6, 15, 16, 17, 18, 19, 20, 22, 23, 24, 34, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56
// Auch hier kann wieder mit Tresholds gearbeitet werden
config.chartsColor1 = [{"else": "F7DC6F"}];
config.chartsColor2 = [{"else": "D7DBDD"}];
config.chartsColor3 = [{"else": "7DCEA0"}];
config.chartsColor4 = [{"else": "85C1E9"}];
config.chartsColor5 = [{"else": "85C1E9"}];
config.chartsColor7 = [{"else": "FFFFFF"}];
config.chartsColor8 = [{"else": "FFFFFF"}];
config.chartsColor9 = [{"else": "FFFFFF"}];
config.chartsColor10 = [{"else": "FFFFFF"}];
config.chartsColor11 = [{"else": "FFFFFF"}];
config.chartsColor21 = [{"else": "FFFFFF"}];
config.chartsColor25 = [{"else": "FFFFFF"}];
config.chartsColor26 = [{"else": "FFFFFF"}];
config.chartsColor27 = [{"else": "FFFFFF"}];
config.chartsColor28 = [{"else": "FFFFFF"}];
config.chartsColor29 = [{"else": "FFFFFF"}];
config.chartsColor30 = [{"else": "FFFFFF"}];
config.chartsColor31 = [{"else": "FFFFFF"}];
config.chartsColor32 = [{"else": "FFFFFF"}];
config.chartsColor33 = [{"else": "FFFFFF"}];
config.chartsColor35 = [{"else": "FFFFFF"}];
// Wie blass oder stark sollen die Graphen sein? (Wert zwischen 0.1 und 1.0)
config.chartsOpacity = [0.4];
//--------------------------------------------------------------------------------------
// Farben der RAHMEN der einzelnen Werte:
// Auch hier kann mit Ausnahme des Timestamps (6) wieder mit Tresholds gearbeitet werden
config.borderColor1 = [{"else": "F7DC6F"}];
config.borderColor2 = [{"else": "D7DBDD"}];
config.borderColor3 = [{"else": "7DCEA0"}];
config.borderColor4 = [{"else": "85C1E9"}];
config.borderColor5 = [{"else": "85C1E9"}];
config.borderColor6 = ["FFFFFF"]; //nur eine Farbe je Instanz!
config.borderColor7 = [{"else": "FFFFFF"}];
config.borderColor8 = [{"else": "FFFFFF"}];
config.borderColor9 = [{"else": "FFFFFF"}];
config.borderColor10 = [{"else": "FFFFFF"}];
config.borderColor11 = [{"else": "FFFFFF"}];
config.borderColor15 = [{"else": "FFFFFF"}];
config.borderColor16 = [{"else": "FFFFFF"}];
config.borderColor17 = [{"else": "FFFFFF"}];
config.borderColor18 = [{"else": "FFFFFF"}];
config.borderColor19 = [{"else": "FFFFFF"}];
config.borderColor20 = [{"else": "FFFFFF"}];
config.borderColor21 = [{"else": "FFFFFF"}];
config.borderColor22 = [{"else": "FFFFFF"}];
config.borderColor23 = [{"else": "FFFFFF"}];
config.borderColor24 = [{"else": "FFFFFF"}];
config.borderColor25 = [{"else": "FFFFFF"}];
config.borderColor26 = [{"else": "FFFFFF"}];
config.borderColor27 = [{"else": "FFFFFF"}];
config.borderColor28 = [{"else": "FFFFFF"}];
config.borderColor29 = [{"else": "FFFFFF"}];
config.borderColor30 = [{"else": "FFFFFF"}];
config.borderColor31 = [{"else": "FFFFFF"}];
config.borderColor32 = [{"else": "FFFFFF"}];
config.borderColor33 = [{"else": "FFFFFF"}];
config.borderColor34 = [{"else": "FFFFFF"}];
config.borderColor35 = [{"else": "FFFFFF"}];
config.borderColor36 = [{"else": "FFFFFF"}];
config.borderColor37 = [{"else": "FFFFFF"}];
config.borderColor38 = [{"else": "FFFFFF"}];
config.borderColor39 = [{"else": "FFFFFF"}];
config.borderColor40 = [{"else": "FFFFFF"}];
config.borderColor41 = [{"else": "FFFFFF"}];
config.borderColor42 = [{"else": "FFFFFF"}];
config.borderColor43 = [{"else": "FFFFFF"}];
config.borderColor44 = [{"else": "FFFFFF"}];
config.borderColor45 = [{"else": "FFFFFF"}];
config.borderColor46 = [{"else": "FFFFFF"}];
config.borderColor47 = [{"else": "FFFFFF"}];
config.borderColor48 = [{"else": "FFFFFF"}];
config.borderColor49 = [{"else": "FFFFFF"}];
config.borderColor50 = [{"else": "FFFFFF"}];
config.borderColor51 = [{"else": "FFFFFF"}];
config.borderColor52 = [{"else": "FFFFFF"}];
config.borderColor53 = [{"else": "FFFFFF"}];
config.borderColor54 = [{"else": "FFFFFF"}];
config.borderColor55 = [{"else": "FFFFFF"}];
config.borderColor56 = [{"else": "FFFFFF"}];
// Wie blass oder stark sollen die Rahmen sein? (Wert zwischen 0.1 und 1.0)
config.borderOpacity = [0.6];
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Graph von https://kevinkub.de/
class LineChart {
constructor(width, height, values) {
this.ctx = new DrawContext();
this.ctx.size = new Size(width, height);
this.values = values;
}
_calculatePath() {
let maxValue = Math.max(...this.values);
let minValue = Math.min(...this.values);
let difference = maxValue - minValue;
let count = this.values.length;
let step = this.ctx.size.width / (count - 1);
let points = this.values.map((current, index, all) => {
let x = step*index;
let y = this.ctx.size.height - (current - minValue) / difference * this.ctx.size.height;
return new Point(x, y);
});
return this._getSmoothPath(points);
}
_getSmoothPath(points) {
let path = new Path();
path.move(new Point(0, this.ctx.size.height));
path.addLine(points[0]);
for(let i = 0; i < points.length-1; i++) {
let xAvg = (points[i].x + points[i+1].x) / 2;
let yAvg = (points[i].y + points[i+1].y) / 2;
let avg = new Point(xAvg, yAvg);
let cp1 = new Point((xAvg + points[i].x) / 2, points[i].y);
let next = new Point(points[i+1].x, points[i+1].y);
let cp2 = new Point((xAvg + points[i+1].x) / 2, points[i+1].y);
path.addQuadCurve(avg, cp1);
path.addQuadCurve(next, cp2);
}
path.addLine(new Point(this.ctx.size.width, this.ctx.size.height));
path.closeSubpath();
return path;
}
configure(fn) {
let path = this._calculatePath();
if(fn) {
fn(this.ctx, path);
} else {
this.ctx.addPath(path);
this.ctx.fillPath(path);
}
return this.ctx;
}
}
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Verarbeite Config-Daten
let instance;
parseConfig();
// Allgemeine Widget-Einstellungen
let widget;
let vStackV;
let vStack0;
let vStack1;
let vStack2;
let vStack3;
let vStack4;
createBasicWidget();
// aktuellen Timestamp erstellen und formatieren
let date = new Date();
let df = new DateFormatter();
df.dateFormat = "HH:mm";
let timestamp = (df.string(date));
let globalData = {"csv": {}, "actuals": {}, "historicals": {}};
try{
// Überprüfen ob openWB erreichbar ist
let url = "http://" + config.oWBip[instance] + "/openWB/web/index.php)";
let result = await stringRequest(url);
await setTitle();
// Inhalt des Widgets
setBackground();
vStack1.addSpacer();
await setField(vStack1, config.panel1[instance]);
if(config.widgetSize[instance] != 0){
vStack1.addSpacer();
await setField(vStack1, config.panel2[instance]);
vStack1.addSpacer();
await setField(vStack1, config.panel3[instance]);
}
vStack1.addSpacer();
vStack2.addSpacer();
if(config.widgetSize[instance] == 0){
await setField(vStack2, config.panel2[instance]);
}
else{
await setField(vStack2, config.panel4[instance]);
vStack2.addSpacer();
await setField(vStack2, config.panel5[instance]);
vStack2.addSpacer();
await setField(vStack2, config.panel6[instance]);
}
vStack2.addSpacer();
if(config.widgetSize[instance] == 2){
vStack3.addSpacer();
await setField(vStack3, config.panel7[instance]);
vStack3.addSpacer();
await setField(vStack3, config.panel8[instance]);
vStack3.addSpacer();
await setField(vStack3, config.panel9[instance]);
vStack3.addSpacer();
vStack4.addSpacer();
await setField(vStack4, config.panel10[instance]);
vStack4.addSpacer();
await setField(vStack4, config.panel11[instance]);
vStack4.addSpacer();
await setField(vStack4, config.panel12[instance]);
vStack4.addSpacer();
vStack5.addSpacer();
await setField(vStack5, config.panel13[instance]);
vStack5.addSpacer();
await setField(vStack5, config.panel14[instance]);
vStack5.addSpacer();
await setField(vStack5, config.panel15[instance]);
vStack5.addSpacer();
}
}
catch(err){
console.log("globaler Fehler: ");
console.log(err);
createBasicWidget();
if(config.imageAtError[instance] == 0){
// Fehlermeldung anzeigen
addDataView(vStack1, "Ein Fehler ist aufgetreten!", "red", "Eingestellte openWB-IP: " + config.oWBip[instance], " ", 0);
addDataView(vStack2, timestamp + " Uhr", "", "Timestamp", " ", 0);
}
else{
// Bild anzeigen
let fm = FileManager.local();
let path = fm.bookmarkedPath(config.errorImageName[instance]);
let errorImage = fm.readImage(path);
widget.backgroundImage = errorImage;
}
}
Script.setWidget(widget);
Script.complete();
// Größe der Widget-Vorschau
switch(config.widgetSize[instance]){
case 0: widget.presentSmall(); break;
case 1: widget.presentMedium(); break;
case 2: widget.presentLarge(); break;
}
//--------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------
// Verarbeite Config-Daten
function parseConfig(){
instance = Number(args.widgetParameter);
if(!instance){
instance = 0;
}
for(let option in config){
// Schreibe nicht-Array Werte an Position 0 des Arrays
if(!Array.isArray(config[option])){
let oldValue = config[option];
config[option] = new Array();
config[option][0] = oldValue;
}
// Schreibe Position 0 des Arrays an nicht vorhandene Instanzwerte
if(!config[option][instance]){
config[option][instance] = config[option][0];
}
}
}
// Allgemeine Widget-Einstellungen
function createBasicWidget(){
widget = new ListWidget();
widget.url = config.widgetURL[instance];
widget.setPadding(20, 20, 20, 20);
let nextRefresh = Date.now() + 1000*30;
widget.refreshAfterDate = new Date(nextRefresh);
// vStackV ist vertikal, vStack0 ist ist Überschrift, vStack1 ist Zeile 1, vStack2 ist Zeile 2
vStackV = widget.addStack();
vStackV.layoutVertically();
vStackV.centerAlignContent();
vStack0 = vStackV.addStack();
vStack0.layoutHorizontally();
vStackV.addSpacer();
vStack1 = vStackV.addStack();
vStack1.layoutHorizontally();
vStackV.addSpacer();
vStack2 = vStackV.addStack();
vStack2.layoutHorizontally();
if(config.widgetSize[instance] == 2){
vStackV.addSpacer();
vStack3 = vStackV.addStack();
vStack3.layoutHorizontally();
vStackV.addSpacer();
vStack4 = vStackV.addStack();
vStack4.layoutHorizontally();
vStackV.addSpacer();
vStack5 = vStackV.addStack();
vStack5.layoutHorizontally();
}
}
// leitet bei Oder-Feldern auf das aktuelle Feld um
async function checkOrPanels(panel){
let LP1leistung = await dataHandler("actuals", "9");
let LP2leistung = await dataHandler("actuals", "10");
switch(panel){
case 12:
if(LP1leistung != 0){
panel = 9;
}
else{
panel = 5;
}
break;
case 13:
if(LP2leistung != 0){
panel = 10;
}
else{
panel = 5;
}
break;
case 14:
if(LP1leistung + LP2leistung != 0){
panel = 11;
}
else{
panel = 5;
}
break;
}
return(panel);
}
// holt, berechnet und speichert alle Daten
async function dataHandler(key1, key2){
// key1: actuals, historicals, csv
// key2: daily, monthly, 0, 1, 2 ...
if(globalData[key1][key2] == undefined){
console.log("Getting data for [" + key1 + "][" + key2 + "]");
let value;
switch(key1){
case "actuals":
switch(key2){
case "0": value = " "; break;
case "1": value = Number(await getRamdiskFile("pvallwatt")*-1); break; // Erzeugung ist negativ
case "2": value = Number(await getRamdiskFile("wattbezug")); break; // Einspeissung ist negativ
case "3": value = Number(await getRamdiskFile("hausverbrauch")); break;
case "4": value = Number(await getRamdiskFile("speichersoc")); break;
case "5": value = Number(await getRamdiskFile("speicherleistung")); break; // Entladen ist negativ
case "6": value = timestamp; break;
case "7": value = Number(await getRamdiskFile("soc")); break;
case "8": value = Number(await getRamdiskFile("soc1")); break;
case "9": value = Number(await getRamdiskFile("llaktuell")); break;
case "10": value = Number(await getRamdiskFile("llaktuells1")); break;
case "11": value = Number(await dataHandler("actuals", "9")) + Number(await dataHandler("actuals", "10")); break;
case "15": value = Number(await getRamdiskFile("aktgeladen")); break;
case "16": value = Number(await getRamdiskFile("aktgeladens1")); break;
case "17": value = Number(await dataHandler("actuals", "15")) + Number(await dataHandler("actuals", "16")); break;
case "18":
csv = await dataHandler("csv", "monthly");
value = (Number(csv[csv.length-2]["LP1"]) - Number(csv[0]["LP1"]))/1000;
break;
case "19":
csv = await dataHandler("csv", "monthly");
value = (Number(csv[csv.length-2]["LP2"]) - Number(csv[0]["LP2"]))/1000;
break;
case "20": value = Number(await dataHandler("actuals", "18")) + Number(await dataHandler("actuals", "19")); break;
case "21": value = Number(await getRamdiskFile("etproviderprice")); break;
case "22": value = Number(await getRamdiskFile("lademodus")); break;
case "23": value = Number(await getRamdiskFile("verbraucher1_watt")); break;
case "24": value = Number(await getRamdiskFile("verbraucher2_watt")); break;
case "25": value = Number(await getRamdiskFile("smarthome_device_1wh0")); break;
case "26": value = Number(await getRamdiskFile("smarthome_device_2wh0")); break;
case "27": value = Number(await getRamdiskFile("smarthome_device_3wh0")); break;
case "28": value = Number(await getRamdiskFile("smarthome_device_4wh0")); break;
case "29": value = Number(await getRamdiskFile("smarthome_device_5wh0")); break;
case "30": value = Number(await getRamdiskFile("smarthome_device_6wh0")); break;
case "31": value = Number(await getRamdiskFile("smarthome_device_7wh0")); break;
case "32": value = Number(await getRamdiskFile("smarthome_device_8wh0")); break;
case "33": value = Number(await getRamdiskFile("smarthome_device_9wh0")); break;
case "34": value = Number(await dataHandler("actuals", "23")) + Number(await dataHandler("actuals", "24")); break;
case "35":
value = 0;
for(let i=25; i<34; i++){
let newValue = Number(await dataHandler("actuals", i.toString()));
if(newValue){
value += newValue;
}
}
break;
case "36": value = Number(await dataHandler("actuals", "34")) + Number(await dataHandler("actuals", "35")); break;
case "37":
csv = await dataHandler("csv", "monthly");
value = Math.round((Number(csv[csv.length-2]["PV"]) - Number(csv[0]["PV"]))/1000);
break;
case "38":
csv = await dataHandler("csv", "monthly");
value = Math.round((Number(csv[csv.length-2]["Einspeisung"]) - Number(csv[0]["Einspeisung"]))/1000);
break;
case "39":
csv = await dataHandler("csv", "monthly");
value = Math.round((Number(csv[csv.length-2]["Bezug"]) - Number(csv[0]["Bezug"]))/1000);
break;
case "40":
csv = await dataHandler("csv", "monthly");
let valueLP40 = (Number(csv[csv.length-2]["LPGesamt"]) - Number(csv[0]["LPGesamt"]))/1000;
value = Math.round(Number(await dataHandler("actuals", "37")) - Number(await dataHandler("actuals", "38")) + Number(await dataHandler("actuals", "39")) - valueLP40);
break;
case "41":
csv = await dataHandler("csv", "monthly");
value = Math.round((Number(csv[csv.length-2]["Laden"]) - Number(csv[0]["Laden"]))/1000);
break;
case "42":
csv = await dataHandler("csv", "monthly");
value = Math.round((Number(csv[csv.length-2]["Entladen"]) - Number(csv[0]["Entladen"]))/1000);
break;
case "43": value = Number(await dataHandler("actuals", "37")) - Number(await dataHandler("actuals", "38")); break;
case "44": value = Number(await getRamdiskFile("daily_pvkwhk")); break;
case "45": value = Number(await getRamdiskFile("daily_einspeisungkwh")); break;
case "46": value = Number(await getRamdiskFile("daily_bezugkwh")); break;
case "47": value = Number(await getRamdiskFile("daily_hausverbrauchkwh")); break;
case "48": value = Number(await getRamdiskFile("daily_sikwh")); break;
case "49": value = Number(await getRamdiskFile("daily_sekwh")); break;
case "50": value = Math.round(Number(await dataHandler("actuals", "44")) - Number(await dataHandler("actuals", "45"))); break;
case "51":
csv = await dataHandler("csv", "monthly");
let valueLP51 = (Number(csv[csv.length-2]["LPGesamt"]) - Number(csv[0]["LPGesamt"]))/1000;
value = Math.round((1 - (Number(await dataHandler("actuals", "39")) / (Number(await dataHandler("actuals", "40")) + valueLP51)))*100);
break;
case "52":
value = Math.round((1 - (Number(await dataHandler("actuals", "39")) / Number(await dataHandler("actuals", "40"))))*100);
break;
case "53":
value = Math.round((1 - (Number(await dataHandler("actuals", "43")) / Number(await dataHandler("actuals", "37"))))*100);
break;
case "54":
let valueLP54 = (Number(csv[csv.length-2]["LPGesamt"]) - Number(csv[0]["LPGesamt"]))/1000;
value = Math.round((1 - (Number(await dataHandler("actuals", "46")) / (Number(await dataHandler("actuals", "47")) + valueLP54)))*100);
break;
case "55":
value = Math.round((1 - (Number(await dataHandler("actuals", "46")) / Number(await dataHandler("actuals", "47"))))*100);
break;
case "56":
value = Math.round((1 - (Number(await dataHandler("actuals", "50")) / Number(await dataHandler("actuals", "44"))))*100);
break;
}
break;
case "historicals":
value = new Array();
switch(key2){
case "1": value = await calcCSVDifference("PV"); break;
case "2":
let valueBezug = await calcCSVDifference("Bezug");
let valueEinspeisung = await calcCSVDifference("Einspeisung");
for(let i=0; i<12; i++){
value[i] = valueBezug[i] - valueEinspeisung[i];
}
break;
case "3":
let valueNetz = await dataHandler("historicals", "2");
let valueSpeicher = await dataHandler("historicals", "5");
let valuePV = await dataHandler("historicals", "1");
// Hausverbrauch = Netzbezug + Speicherentladung + PV-Erzeugung - Netzeinspeisung - Speicherladung
// (Netz = Netzbezug - Netzeinspeisung, Speicher = Laden - Entladen)
for(let i=0; i<12; i++){
value[i] = valueNetz[i] + valuePV[i] + valueSpeicher[i];
}
break;
case "4": value = await calcCSVDifference("SoC"); break;
case "5":
let valueLaden = await calcCSVDifference("Laden");
let valueEntladen = await calcCSVDifference("Entladen");
for(let i=0; i<12; i++){
value[i] = valueLaden[i] - valueEntladen[i];
}
break;
case "7": value = await calcCSVDifference("SoC1"); break;
case "8": value = await calcCSVDifference("SoC2"); break;
case "9": value = await calcCSVDifference("LP1"); break;
case "10": value = await calcCSVDifference("LP2"); break;
case "11":
let valueLP1 = await dataHandler("historicals", "9");
let valueLP2 = await dataHandler("historicals", "10");
for(let i=0; i<12; i++){
value[i] = valueLP1[i] + valueLP2[i];
}
break;
case "21":
let awattarRaw = Number(await getRamdiskFile("etprovidergraphlist"));
let lines = awattarRaw.split("\n");
for(let i = 0; i < 10; i++){
let seperated = lines[i].split(",");
value[i] = Number(seperated[1]);
}
case "25": value = await calcCSVDifference("Smarthome1"); break;
case "26": value = await calcCSVDifference("Smarthome2"); break;
case "27": value = await calcCSVDifference("Smarthome3"); break;
case "28": value = await calcCSVDifference("Smarthome4"); break;
case "29": value = await calcCSVDifference("Smarthome5"); break;
case "30": value = await calcCSVDifference("Smarthome6"); break;
case "31": value = await calcCSVDifference("Smarthome7"); break;
case "32": value = await calcCSVDifference("Smarthome8"); break;
case "33": value = await calcCSVDifference("Smarthome9"); break;
case "35":
for(let i=25; i<34; i++){
let newValue = dataHandler("historical", i.toString());
for(let i=0; i<12; i++){
value[i] += newValue[i];
}
}
break;
}
break;
case "csv":
switch(key2){
case "daily": value = await getDailyCSV(); break;
case "monthly": value = await getMonthlyCSV(); break;
}
break;
}
if(value != undefined){
globalData[key1][key2] = value;
}
else{
throw "Daten zu [" + key1 + "][" + key2 + "] nicht gefunden!";
}
}
return globalData[key1][key2];
}
// belegt die Felder
async function setField(widget, panel){
if(panel == 12 || panel == 13 || panel == 14){
panel = await checkOrPanels(panel);
}
let valueColor;
let footnote;
let borderColor;
let chart;
let border;
let actuals;
let value;
let name;
const chartsNotSupported = [0, 6, 15, 16, 17, 18, 19, 20, 22, 23, 24, 34, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56];
const metaData = {
"0":{
"name": " ",
"unit": "",
"footnote": " "
},
"1":{
"name": "PV",
"unit": "W"
},
"2":{
"name": "Netz",
"unit": "W"
},
"3":{
"name": "Haus",
"unit": "W"
},
"4":{
"name": "SoC",
"unit": "%"
},
"5":{
"name": "Speicher",
"unit": "W"
},
"6":{
"name": "Timestamp",
"unit": ""
},
"7":{
"name": "LP1 SoC",
"unit": "%"
},
"8":{
"name": "LP2 SoC",
"unit": "%"
},
"9":{
"name": "LP1 Leist.",
"unit": "W"
},
"10":{
"name": "LP2 Leist.",
"unit": "W"
},
"11":{
"name": "LP1+2 Leist.",
"unit": "W"
},
"15":{
"name": "LP1",
"unit": "kWh",
"footnote": "zuletzt",
},
"16":{
"name": "LP2",
"unit": "kWh",
"footnote": "zuletzt",
},
"17":{
"name": "LP1+2",
"unit": "kWh",
"footnote": "zuletzt",
},
"18":{
"name": "LP1",
"unit": "kWh",
"footnote": "Monat"
},
"19":{
"name": "LP2",
"unit": "kWh",
"footnote": "Monat"
},
"20":{
"name": "LP 1+2",
"unit": "kWh",
"footnote": "Monat"
},
"21":{
"name": "Strompreis",
"unit": "ct"
},
"22":{
"name": "Lademodus",
"unit": ""
},
"23":{
"name": "Verbr. 1",
"unit": "W"
},
"24":{
"name": "Verbr. 2",
"unit": "W",
},
"25":{
"name": "Gerät 1",
"unit": "W"
},
"26":{
"name": "Gerät 2",
"unit": "W"
},
"27":{
"name": "Gerät 3",
"unit": "W"
},
"28":{
"name": "Gerät 4",
"unit": "W"
},
"29":{
"name": "Gerät 5",
"unit": "W"
},
"30":{
"name": "Gerät 6",
"unit": "W"
},
"31":{
"name": "Gerät 7",
"unit": "W"
},
"32":{
"name": "Gerät 8",
"unit": "W"
},
"33":{
"name": "Gerät 9",
"unit": "W"
},
"34":{
"name": "Verbraucher",
"unit": "W",
"footnote": "1 + 2"
},
"35":{
"name": "Geräte",
"unit": "W",
"footnote": "1 - 9"
},
"36":{
"name": "Smarthome",
"unit": "W",
"footnote": "Verbr. + Geräte"
},
"37":{
"name": "PV",
"unit": "kWh",
"footnote": "Monat",
},
"38":{
"name": "Einspeisung",
"unit": "kWh",
"footnote": "Monat",
},
"39":{
"name": "Bezug",
"unit": "kWh",
"footnote": "Monat",
},
"40":{
"name": "Haus",
"unit": "kWh",
"footnote": "Monat",
},
"41":{
"name": "Laden",
"unit": "kWh",
"footnote": "Monat",
},
"42":{
"name": "Entladen",
"unit": "kWh",
"footnote": "Monat",
},
"43":{
"name": "Eigenverbrauch",
"unit": "kWh",
"footnote": "Monat",
},
"44":{
"name": "PV",
"unit": "kWh",
"footnote": "heute",
},
"45":{
"name": "Einspeisung",
"unit": "kWh",
"footnote": "heute",
},
"46":{
"name": "Bezug",
"unit": "kWh",
"footnote": "heute",
},
"47":{
"name": "Haus",
"unit": "kWh",
"footnote": "heute",
},
"48":{
"name": "Laden",
"unit": "kWh",
"footnote": "heute",
},
"49":{
"name": "Entladen",
"unit": "kWh",
"footnote": "heute",
},
"50":{
"name": "Eigenverbrauch",
"unit": "kWh",
"footnote": "heute",
},
"51":{
"name": "AQ Haus",
"unit": "%",
"footnote": "Monat",
},
"52":{
"name": "AQ Haus+LP",
"unit": "%",
"footnote": "Monat",
},
"53":{
"name": "EQ",
"unit": "%",
"footnote": "Monat",
},
"54":{
"name": "AQ Haus",
"unit": "%",
"footnote": "heute",
},
"55":{
"name": "AQ Haus+LP",
"unit": "%",
"footnote": "heute",
},
"56":{
"name": "EQ",
"unit": "%",
"footnote": "heute",
},
};
try{
console.log("----- Panel " + panel + " -----");
// Allgemeines
border = config.panelBorder[instance];
actuals = await dataHandler("actuals", panel.toString());
value = actuals.toString() + metaData[panel.toString()]["unit"];
name = metaData[panel.toString()]["name"];
// Footnote
if(metaData[panel.toString()]["footnote"] != undefined){
footnote = metaData[panel.toString()]["footnote"];
}
else{
switch(panel){
case 2: footnote = calcFootnote(100, -100, "Bezug", "Einspeisen", "Speicherregelung", actuals); break;
case 5: footnote = calcFootnote(50, -50, "Laden", "Entladen", "Netzregelung", actuals); break;
case 9: if(config.lademodusFootnote[instance] == 1) footnote = calcLademodusFootnote(await dataHandler("actuals", "22")); break;
case 10: if(config.lademodusFootnote[instance] == 1) footnote = calcLademodusFootnote(await dataHandler("actuals", "22")); break;
case 11: if(config.lademodusFootnote[instance] == 1) footnote = calcLademodusFootnote(await dataHandler("actuals", "22")); break;
case 21: footnote = calcAwattarFootnote(); break;
case 22: footnote = calcLademodusFootnote(actuals); break;
case 23: footnote = (await getRamdiskFile("verbraucher1_name")).trim(); break;
case 23: footnote = (await getRamdiskFile("verbraucher2_name")).trim(); break;
default:
footnote = " ";
}
}
// Farbe der Werte
if(config.hasOwnProperty("textColor" + panel) && !isNaN(Number(actuals))){
valueColor = await calcColor(config["textColor" + panel][instance], actuals);
}
else if(panel == 6){
valueColor = config.textColor6[instance];
}
else{
valueColor = config.defaultColor[instance];
}
// Farben der Rahmen
if(config.hasOwnProperty("borderColor" + panel) && !isNaN(Number(actuals))){
borderColor = await calcColor(config["borderColor" + panel][instance], actuals);
}
else if(panel == 6){
borderColor = config.borderColor6[instance];
}
else{
borderColor = config.defaultColor[instance];
}
// Charts
if(config.hasOwnProperty("chartsColor" + panel) && !isNaN(Number(actuals)) && !chartsNotSupported.includes(panel)){
let historicals = await dataHandler("historicals", panel.toString());
let chartsColor = await calcColor(config["chartsColor" + panel][instance], actuals);
chart = createChart(historicals, stringToColor(chartsColor, config.chartsOpacity[instance]));
}
else{
chart = 0;
}
// Platzhalter
if(panel == 0){
border = 0;
borderColor = "FFFFFF";
chart = 0;
}
}
catch(err){
console.log("Fehler bei Feld " + panel + " : ");
console.log(err);
valueColor = "";
name = " ";
footnote = " ";
borderColor = "FFFFFF";
chart = 0;
if(config.placeholderAtError[instance]){
value = " ";
border = 0;
}
else{
value = "Fehler!";
}
}
addDataView(widget, value, valueColor, name, footnote, borderColor, chart, border);
}
// legt Text des Lademodus fest
function calcLademodusFootnote(modus){
switch(modus){
case 0:
footnote = "Sofortladen";
break;
case 1:
footnote = "Min & PV";
break;
case 2:
footnote = "Nur PV";
break;
case 3:
footnote = "Stop";
break;
case 4:
footnote = "Standby";
break;
default:
footnote = " ";
break;
}
return(footnote);
}
// erstellt Footnote für Awattar
function calcAwattarFootnote(){
let hoursDate = new Date();
let hoursDate2 = new Date();
hoursDate2.setHours(hoursDate2.getHours() + 1);
let hoursDF = new DateFormatter();
hoursDF.dateFormat = "HH";
let hours = (hoursDF.string(hoursDate));
let hours2 = (hoursDF.string(hoursDate2));
footnote = hours + " - " + hours2 + " Uhr";
return(footnote);
}
// legt Hintergrundfarbe oder Verlauf fest
function setBackground(){
if(config.backgroundType[instance] == 0){
widget.backgroundColor = new Color(config.backgroundColor[instance]);
}
else{
const gradient = new LinearGradient()
gradient.locations = [0, 1]
gradient.colors = [ new Color(config. backgroundGradient1[instance]), new Color(config. backgroundGradient2[instance]) ]
widget.backgroundGradient = gradient
}
}
// Erstellt den Widget-Titel mit Überschrift, Logo und Timestamp
async function setTitle(){
if(config.titleAlignment[instance] == 1){
vStack0.addSpacer();
}
else{
vStack0.addSpacer(10);
}
if(config.titleAlignment[instance] == 1 && config.widgetSize[instance] != 0){
if(config.timestampTitle[instance] == 0){
if(config.widgetTitle[instance] && config.logo[instance]){
vStack0.addSpacer(36);
}
else{
vStack0.addSpacer(32);
}
}
else{
if(config.widgetTitle[instance] && config.logo[instance]){
vStack0.addSpacer(70);
}
else{
vStack0.addSpacer(66);
}
}
}
// Logo holen und darstellen
if(config.logo[instance] == 1){
img = vStack0.addImage(await getLogo());
img.imageSize = new Size(50,20);
}
// Überschrift des Widgets
if((config.imageAtError[instance]!= 1) && ((config.widgetSize[instance] == 0 && config.logo[instance] == 1)!= 1) && config.widgetTitle[instance]){
if(config.logo[instance] == 1){
vStack0.addSpacer(10);
}
let header = vStack0.addText(config.widgetTitle[instance]);
header.font = Font.mediumSystemFont(12);
header.textColor = new Color(config.defaultColor[instance]);
}
vStack0.addSpacer();
// Timestamp in Titel
if(config.timestampTitle[instance] == 1 && config.widgetSize[instance] != 0){
let ts = vStack0.addText(timestamp);
ts.font = Font.mediumSystemFont(12);
ts.textColor = new Color(config.defaultColor[instance]);
}
}
// Holt das Logo von der openWB und bearbeitet dieses
async function getLogo(){
let result;
let imgReq = new Request("http://" + config.oWBip[instance] + "/openWB/web/img/favicons/apple-icon-180x180.png");
imgReq.timeoutInterval = config.timeOut[instance];
let image = await imgReq.loadImage();
dc = new DrawContext();
dc.opaque = true;
dc.size = new Size(180,56);
dc.drawImageAtPoint(image, new Point(-0, -54));
result = dc.getImage();
return(result);
}
// zentriert/linksbündigt/rechtbündigt Text
function alignStack(stack, pos){
if(config.contentAlignment[instance] == 1 || (config.contentAlignment[instance] == 0 && pos == 1)){
stack.addSpacer();
}
}
// legt ein neues Datenstack an
function addDataView(widget, data, color, name, foot, borderColor, img, border){
let viewStack = widget.addStack();
viewStack.layoutVertically();
viewStack.cornerRadius = 5;
if(borderColor != 0){
viewStack.size = new Size(97, 47);
viewStack.borderWidth = border;
viewStack.borderColor = stringToColor(borderColor, config.borderOpacity[instance]);
viewStack.setPadding(5, 5, 5, 3);
}
let labelStack = viewStack.addStack();
alignStack(labelStack, 0);
let label = labelStack.addText(name);
label.font = Font.mediumSystemFont(12);
label.textColor = new Color(config.defaultColor[instance]);
alignStack(labelStack, 1);
let footnoteStack = viewStack.addStack();
alignStack(footnoteStack, 0);
let footnote = footnoteStack.addText(foot);
footnote.font = Font.mediumSystemFont(8);
footnote.textColor = new Color(config.defaultColor[instance]);
alignStack(footnoteStack, 1);
let valueStack = viewStack.addStack();
alignStack(valueStack, 0);
let value = valueStack.addText(data);
value.font = Font.mediumSystemFont(18);
value.textColor = stringToColor(color, 1);
alignStack(valueStack, 1);
if(config.backgroundCharts[instance] == 1){
if(img != 0){
viewStack.backgroundImage = img;
}
}
}
// holt Daten von der Ramdisk
async function getRamdiskFile(file){
let url = "http://" + config.oWBip[instance] + "/openWB/ramdisk/" + file;
let result = await stringRequest(url);
return(result);
}
// führt eigenlichen HTTP-Request aus
async function stringRequest(url){
let result = 0;
let req = new Request(url);
req.timeoutInterval = config.timeOut[instance];
result = await req.loadString();
return(result);
}
// Gibt die Farbe des zutreffenden Tresholds zurück
async function calcColor(colorRule, value){
// Ersetzte $... Werte
for(let key in colorRule){
let oldKey = key;
for(let i = 56; i>-1; i--){
let searchPattern = "$" + i.toString();
if(key.includes(searchPattern)){
let replacement = await dataHandler("actuals", i.toString());
//console.log("Ersetze " + searchPattern + " durch " + replacement);
key = key.replace(searchPattern, replacement);
}
}
if(key != "else") key = eval(key);
if(key != oldKey){
colorRule[key] = colorRule[oldKey];
delete colorRule[oldKey];
}
//console.log("vor: " + oldKey + ", nach: " + key);
}
let tresholds = Object.keys(colorRule);
tresholds.sort(function(a, b){
return a - b;
});
tresholds.reverse();
// Beginne mit höchstem Wert, Rückgabe der Farbe des ersten überschittenen Tresholds
for(let treshold of tresholds){
if(!isNaN(Number(treshold))){
if(value >= Number(treshold)){
//console.log("gewählter Treshold: " + treshold);
return colorRule[treshold];
}
}
}
// Wenn kein Treshold passt, probiere "else", sonst Standard-Textfarbe
if(tresholds.includes("else")){
return colorRule["else"];
}
return config.defaultColor[instance];
}
// Legt Footnote fest
function calcFootnote(groesserAls, kleinerAls, groesserText, kleinerText, sonstText, value){
if (value > groesserAls){
return(groesserText);
}
else if (value < kleinerAls){
return(kleinerText);
}
else{
return(sonstText);
}
}
// Gibt das entsprechende Color-Objekt zurück
function stringToColor(colorString, opacity){
if(colorString == "red"){
colorString = "FF453A";
}
if(colorString == "yellow"){
colorString = "FFD60A";
}
if(colorString == "green"){
colorString = "30D158";
}
if(isHexColor(colorString)){
return (new Color(colorString, opacity));
}
return (new Color(config.defaultColor[instance], opacity));
}
// Überprüft, ob der String eine Hex-Farbe darstellt
function isHexColor(hex){
return typeof hex === 'string'
&& hex.length === 6
&& !isNaN(Number('0x' + hex))
}
// Erstellt Graph und liefert Bild davon zurück
function createChart(data, color){
if(config.backgroundCharts[instance] == 1){
let chart = new LineChart(97, 47, data).configure((ctx, path) => {
ctx.opaque = false;
ctx.setFillColor(color);
ctx.addPath(path);
ctx.fillPath(path);
}).getImage();
return(chart);
}
}
// Holt CSV-Daten von oWB
async function getCSVFile(type, date){
let df = new DateFormatter();
switch(type){
case "daily":
df.dateFormat = "YYYYMMdd";
break;
case "monthly":
df.dateFormat = "YYYYMM";
break;
}
let fdate = (df.string(date));
let url = ("http://" + config.oWBip[instance] + "/openWB/web/logging/data/" + type + "/" + fdate + ".csv");
let result = await stringRequest(url);
return(result);
}
// gibt zusammengesetzte CSV-Daten des aktuellen+vorherigen Tages als Object zurück
async function getDailyCSV(){
let date = new Date();
let csvToday = await getCSVFile("daily", date);
date.setDate(date.getDate()-1)
let csvYesterday = await getCSVFile("daily", date);
let csv = csvYesterday + csvToday;
let result = CSVToObject(csv, ["Zeit", "Bezug", "Einspeisung", "PV", "LP1", "LP2", "LP3", "LPGesamt", "Laden", "Entladen", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "SoC", "SoC1", "SoC2", "24", "25", "26", "Smarthome1", "Smarthome2", "Smarthome3", "Smarthome4", "Smarthome5", "Smarthome6", "Smarthome7", "Smarthome8", "Smarthome9", "36", "37", "38", "39"]);
return(result);
}
// gibt CSV-Daten des aktuellen Monats als Object zurück
async function getMonthlyCSV(){
let date = new Date();
let csv = await getCSVFile("monthly", date);
let result = CSVToObject(csv, ["1", "Bezug", "Einspeisung", "PV", "LP1", "LP2", "LP3", "LPGesamt", "9", "10", "11", "12", "13", "14", "15", "16", "17", "Laden", "Entladen", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29"]);
return(result);
}
// Kalkuliert die Differenz zwischen den Werten der letzten Stunde
async function calcCSVDifference(key){
let result = new Array();
if(config.backgroundCharts[instance] == 1){
let csv = await dataHandler("csv", "daily");
let resultVar = 11;
for(let i=0; i<12; i++){
let a = Number(csv[csv.length-2-i][key]);
let b = Number(csv[csv.length-3-i][key]);
if(key != "SoC" && key != "SoC1" && key != "SoC2"){
result[resultVar] = Number(a - b);
}
else{
result[resultVar] = a;
}
resultVar --;
}
}
return(result);
}
// Konvertiert CSV zu einem Objekt
function CSVToObject(csv, headers){
let lines = csv.split("\n");
let result = [];
for(let i=0;i<lines.length;i++){
let obj = {};
let currentline=lines[i].split(",");
for(let j=0;j<headers.length;j++){
obj[headers[j]] = currentline[j];
}
result.push(obj);
}
return(result);
}
@jscmidt
Copy link
Author

jscmidt commented May 3, 2021

iOS-Widget für openWB mit der Scriptable-App.

Mit dem Widget können eine Vielzahl an Werten der openWB angezeigt werden. Die Werte können je nach Bedarf frei auf dem Widget verteilt werden, und auch sonst stehen viele individuelle Konfigurationsmöglichkeiten zur Verfügung, so können z.B. alle verwendete Farben frei nach den eigenen Wünschen angepasst werden. Ein paar Beispiele gibt es hier:

kleines Widget

mittleres Widget

großes Widget

Anleitung

1. Scriptable-App herunterladen

Die App ist hier im Appstore verfügbar.

2. Skript in der Scriptable-App einrichten

Dazu muss das Skript zuerst Raw geöffnet werden:
Raw öffnen
Dann alles markieren (ja, das ist leider mühsam) und kopieren.
Jetzt die Scriptable-App öffnen, über das + oben rechts ein neues Skript erstellen und den zuvor kopierten Text einfügen.
Anschließend unbedingt hinter oWBip die IP der openWB in Anführungszeichen eintragen und unter widgetURL die Internetseite, die beim Tippen auf das Widget geöffnet werden soll. Hier bietet sich die openWB-Startseite an.
Alle anderen Parameter am Anfang des Skripts können nach Bedarf angepasst werden, die Optionen sollten mit den Erklärtexten eigentlich selbsterklärend sein.
Wichtig: Die Widgets unter iOS 14 stehen in drei verschiedenen Größen zur Verfügung. Damit das Widget richtig skaliert angezeigt wird, muss nach widgetSize die gewünschte Größe eingetragen werden. Standardmäßig ist die mittlere Größe eingestellt.
Durch Tippen auf Untitled Script (ganz oben) kann noch der Name des Skripts geändert werden, danach durch Tippen auf Done abspeichern.

3. Widget auf dem Home-Bildschirm einrichten

Zuerst muss ein Scriptable-Widget zum Homescreen hinzugefügt werden. Dafür lange auf ein existierendes Widget oder eine App tippen, dann auf Home-Bildschirm bearbeiten tippen und dann mit dem + oben links ein Scriptable-Widget in der gewünschten (und zuvor eingestellten!) Größe hinzufügen.
Nachdem dieses Widget hinzugefügt wurde den Bearbeitungsmodus verlassen (Home-Button).
Danach lange auf das neue Scriptable-Widget tippen, Widget bearbeiten auswählen und unter Script den zuvor verwendeten Namen auswählen. Alle anderen Werte können so gelassen werden.

@jscmidt
Copy link
Author

jscmidt commented Aug 31, 2021

Diskussionen zum Widget gibt es hier im openWB-Forum.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment