@@ -335,12 +335,25 @@
< input type = "text" id = "modal-name" placeholder = "z.B. Dach Süd" > < / div >
< div class = "field" > < label > Gerätetyp< / label >
< select id = "modal-model" > < / select > < / div >
< div class = "field" > < label > IP-Adresse < / label >
< input type = "text" id = "modal-ip" placeholder = "10.10.20.190" > < / div >
< div class = "field" > < label > Modbus Port < / label >
< input type = "number" id = "modal-port" value = "502" > < / div >
< div class = "field" > < label > Modbus Slave-Adresse < / label >
< input type = "number" id = "modal-addr" value = "1" min = "1" max = "247" > < / div >
< div id = "modbus- fields " >
< div class = "field" > < label > IP-Adresse < / label >
< input type = "text" id = "modal-ip" placeholder = "10.10.20.190" > < / div >
< div class = "field" > < label > Modbus Port < / label >
< input type = "number" id = "modal-port" value = "502" > < / div >
< div class = "field" > < label > Modbus Slave-Adresse < / label >
< input type = "number" id = "modal-addr" value = "1" min = "1" max = "247" > < / div >
< / div >
< div id = "ha-entity-fields" style = "display:none" >
< div class = "field" > < label > HA Entity-Prefix< / label >
< input type = "text" id = "modal-ha-prefix" placeholder = "growatt_wifi_x_1" >
< div style = "font-size:11px;color:var(--muted);margin-top:3px" > Ohne "sensor."-Präfix — z.B. bei < code > sensor.growatt_wifi_x_1_dcpower< / code > ist der Wert < code > growatt_wifi_x_1< / code > . Braucht ein bereits per ESPHome in HA eingebundenes Gerät.< / div > < / div >
< div class = "field" > < label > Home Assistant URL< / label >
< input type = "text" id = "modal-ha-url" placeholder = "http://localhost:8123" >
< div style = "font-size:11px;color:var(--muted);margin-top:3px" > Meist unverändert lassen — dank host_network erreicht ShineBridge HA Core direkt über localhost.< / div > < / div >
< div class = "field" > < label > Long-Lived Access Token< / label >
< input type = "password" id = "modal-ha-token" placeholder = "Einstellungen → Profil → Sicherheit → Langlebige Zugriffs-Token" >
< div style = "font-size:11px;color:var(--muted);margin-top:3px" > Kein Supervisor-Token nutzbar (host_network) — braucht ein manuell in HA erstelltes Token.< / div > < / div >
< / div >
< div class = "field" > < label > MQTT Topic-Präfix< / label >
< input type = "text" id = "modal-prefix" placeholder = "growatt/wechselrichter1" > < / div >
< div class = "field" > < label > Abfrageintervall (Sekunden)< / label >
@@ -736,6 +749,122 @@ function renderEnergy(inverters, aggregates, period, spotData) {
// ── Finance Tab ───────────────────────────────────────────────
// Erzeugt plausible, aber frei erfundene Tagesdaten fürs Beispiel-Diagramm im
// Leerzustand (Erstinstallation). Deterministisch (kein echter Zufall), damit
// die Vorschau bei jedem Laden gleich aussieht statt bei jedem Reload zu springen.
function exampleFinanceDays ( n ) {
const out = [ ] ;
const today = new Date ( ) ;
for ( let i = n - 1 ; i >= 0 ; i -- ) {
const d = new Date ( today ) ; d . setDate ( d . getDate ( ) - i ) ;
const wave = 4.2 + 2.6 * Math . sin ( i * 0.6 ) + ( i % 3 === 0 ? 1.4 : 0 ) ;
const fixed = Math . max ( 1.5 , wave ) ;
const spot = Math . max ( 0.8 , fixed * ( 0.68 + 0.1 * Math . cos ( i * 0.9 ) ) ) ;
out . push ( { date : d . toISOString ( ) . slice ( 0 , 10 ) , fixed _eur : + fixed . toFixed ( 2 ) , spot _eur : + spot . toFixed ( 2 ) } ) ;
}
return out ;
}
// Baut das Kosten-Balkendiagramm (echte oder Beispiel-Daten). Aggregiert ab
// 14 Wochen (98 Tagen) angesammelter Historie automatisch von Tages- auf
// Wochenbalken; zeigt davon je nach verfügbarer Breite die letzten 10– 28
// Perioden (rollierendes Fenster — ältere Balken wandern rechts raus, sobald
// neue dazukommen).
function buildCostChart ( days , el , isExample ) {
const C = isExample
? { fixed : '#5b6472' , spot : '#6b6155' , grid : '#262b33' }
: { fixed : '#58a6ff' , spot : '#f0883e' , grid : '#30363d' } ;
const rawN = days . length ;
const bucket = rawN >= 98 ? 7 : 1 ;
const allPeriods = bucket === 1 ? days . map ( d => ( {
fixed _eur : d . fixed _eur , spot _eur : d . spot _eur , label : d . date . slice ( 5 ) ,
} ) ) : ( ( ) => {
const out = [ ] ;
for ( let i = 0 ; i < days . length ; i += bucket ) {
const chunk = days . slice ( i , i + bucket ) ;
const fixed = chunk . reduce ( ( s , d ) => s + ( d . fixed _eur || 0 ) , 0 ) ;
const spotVals = chunk . map ( d => d . spot _eur ) . filter ( v => v != null ) ;
const spot = spotVals . length ? spotVals . reduce ( ( a , b ) => a + b , 0 ) : null ;
out . push ( { fixed _eur : fixed , spot _eur : spot , label : chunk [ 0 ] . date . slice ( 5 ) } ) ;
}
return out ;
} ) ( ) ;
// Container-Breite messen und die SVG in echten Pixeln in genau dieser Breite
// zeichnen (keine viewBox-Skalierung) — Höhe bleibt dabei fix, unabhängig von
// der Breite. Auf breiten Screens werden mehr Perioden gezeigt statt dieselben
// künstlich zu verbreitern.
const H = 240 , PL = 40 , PR = 8 , PT = 14 , PB = 46 ;
const measuredW = el . clientWidth || 380 ;
const W = Math . max ( 300 , measuredW ) ;
const cW = W - PL - PR ;
const IDEAL _SLOT = 24 ; // px/Periode, entspricht dem Slot, der auf Handy-Breite gut aussah
const maxPeriods = Math . min ( 28 , Math . max ( 10 , Math . round ( cW / IDEAL _SLOT ) ) ) ;
const periods = allPeriods . slice ( - maxPeriods ) ;
const n = periods . length ;
const cH = H - PT - PB ;
const slot = cW / n ;
const barW = Math . max ( 3 , Math . min ( 26 , slot * 0.36 ) ) ;
const barGap = Math . max ( 1.5 , slot * 0.08 ) ;
const rBar = Math . min ( 2.5 , barW / 2 ) ;
const allVals = periods . flatMap ( d => [ d . fixed _eur , d . spot _eur ] . filter ( v => v != null ) ) ;
const maxVal = Math . max ( ... allVals , 0.01 ) ;
const yTicks = 4 ;
let gridLines = '' , yLabels = '' ;
for ( let i = 0 ; i <= yTicks ; i ++ ) {
const v = maxVal * i / yTicks ;
const y = PT + cH - ( cH * i / yTicks ) ;
gridLines += ` <line x1=" ${ PL } " y1=" ${ y . toFixed ( 1 ) } " x2=" ${ W - PR } " y2=" ${ y . toFixed ( 1 ) } " stroke=" ${ C . grid } " stroke-width="1" stroke-dasharray="2,3"/> ` ;
yLabels += ` <text x=" ${ PL - 6 } " y=" ${ ( y + 3 ) . toFixed ( 1 ) } " text-anchor="end" font-size="9" fill="#8b949e"> ${ fEur ( v ) } </text> ` ;
}
const gradId = isExample ? 'Ex' : '' ;
let bars = '' , xLabels = '' ;
periods . forEach ( ( d , i ) => {
const cx = PL + i * slot + slot / 2 ;
const hFixed = Math . max ( 0 , cH * d . fixed _eur / maxVal ) ;
bars += ` <rect x=" ${ ( cx - barGap / 2 - barW ) . toFixed ( 1 ) } " y=" ${ ( PT + cH - hFixed ) . toFixed ( 1 ) } " width=" ${ barW . toFixed ( 1 ) } " height=" ${ Math . max ( hFixed , 1 ) . toFixed ( 1 ) } " rx=" ${ rBar } " fill="url(#gradFixed ${ gradId } )"/> ` ;
if ( d . spot _eur != null ) {
const hSpot = Math . max ( 0 , cH * d . spot _eur / maxVal ) ;
bars += ` <rect x=" ${ ( cx + barGap / 2 ) . toFixed ( 1 ) } " y=" ${ ( PT + cH - hSpot ) . toFixed ( 1 ) } " width=" ${ barW . toFixed ( 1 ) } " height=" ${ Math . max ( hSpot , 1 ) . toFixed ( 1 ) } " rx=" ${ rBar } " fill="url(#gradSpot ${ gradId } )"/> ` ;
}
const ly = H - PB + 12 ;
xLabels += ` <text x=" ${ cx . toFixed ( 1 ) } " y=" ${ ly } " text-anchor="end" font-size="8.5" fill="#8b949e" transform="rotate(-40 ${ cx . toFixed ( 1 ) } ${ ly } )"> ${ d . label } </text> ` ;
} ) ;
const defs = ` <defs>
<linearGradient id="gradFixed ${ gradId } " x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color=" ${ C . fixed } " stop-opacity="0.95"/>
<stop offset="100%" stop-color=" ${ C . fixed } " stop-opacity="0.55"/>
</linearGradient>
<linearGradient id="gradSpot ${ gradId } " x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color=" ${ C . spot } " stop-opacity="0.95"/>
<stop offset="100%" stop-color=" ${ C . spot } " stop-opacity="0.55"/>
</linearGradient>
</defs> ` ;
const bucketLabel = bucket === 1 ? 'Tag' : 'Woche' ;
const rangeNote = allPeriods . length > n ? ` · letzte ${ n } ` : '' ;
const legend = ` <div style="display:flex;gap:18px;margin-top:10px;font-size:11px;color:var(--text-dim)">
<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background: ${ C . fixed } ;display:inline-block"></span>Festpreis</span>
<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background: ${ C . spot } ;display:inline-block"></span>Spot (hypothetisch)</span>
</div> ` ;
return `
<div style="position:relative;border-top:1px solid var(--border);padding-top:24px;margin-top:4px">
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:14px">Kosten je ${ bucketLabel } ${ rangeNote } </div>
<svg width=" ${ W } " height=" ${ H } " style="display:block">
${ defs } ${ gridLines } ${ yLabels } ${ bars } ${ xLabels }
</svg>
${ legend }
${ isExample ? ` <div style="position:absolute;top:20px;right:0;font-size:10px;font-weight:700;letter-spacing:.08em;color:var(--text-dim);background:var(--surface2);border:1px solid var(--border);border-radius:20px;padding:3px 10px">BEISPIEL</div> ` : '' }
</div> ` ;
}
async function loadFinance ( ) {
const el = document . getElementById ( "finance-content" ) ;
let data ;
@@ -746,10 +875,14 @@ async function loadFinance() {
const { days , fixed _total _eur , spot _total _eur , savings _eur , period _start , spot _days , total _days } = data ;
if ( ! days || days . length === 0 ) {
el . innerHTML = ` <div style="text-align:center;padding:40px;color:var(--text-dim)">
<div style="font-size:32px;margin-bottom:12px">📊</div >
<div style="font-weight:600 ;margin-bottom:6 px">Noch keine Daten</div>
<div style="font-size:13px">Der Tracker sammelt ab morgen täglich Daten.<br>Abrechnungsjahr ab ${ period _start } . </div>
const exampleChart = buildCostChart ( exampleFinanceDays ( 14 ) , el , true ) ;
el . innerHTML = ` <div style="padding:4px 0" >
<div style="text-align:center;padding:28px 20px;color:var(--text-dim);border:1px dashed var(--border);border-radius:var(--radius) ;margin-bottom:24 px">
<div style="font-size:28px;margin-bottom:10px">📊 </div>
<div style="font-weight:600;margin-bottom:6px;color:var(--text)">Noch keine echten Daten</div>
<div style="font-size:13px">Der Tracker sammelt ab morgen täglich Daten.<br>Abrechnungsjahr ab ${ period _start } . Unten siehst du, wie die Ansicht dann aussieht.</div>
</div>
${ exampleChart }
</div> ` ;
return ;
}
@@ -796,99 +929,7 @@ async function loadFinance() {
${ spotCard } ${ savCard }
</div> ` ;
// ── SVG-Balkendiagramm ────────────────────────────────────────
// Bei langen Abrechnungsjahren nicht jeden einzelnen Tag zeigen — statt 112
// Tagesbalken lieber Wochenbalken.
const rawN = days . length ;
const bucket = rawN > 180 ? 14 : rawN > 21 ? 7 : 1 ;
const allPeriods = bucket === 1 ? days . map ( d => ( {
fixed _eur : d . fixed _eur , spot _eur : d . spot _eur , label : d . date . slice ( 5 ) ,
} ) ) : ( ( ) => {
const out = [ ] ;
for ( let i = 0 ; i < days . length ; i += bucket ) {
const chunk = days . slice ( i , i + bucket ) ;
const fixed = chunk . reduce ( ( s , d ) => s + ( d . fixed _eur || 0 ) , 0 ) ;
const spotVals = chunk . map ( d => d . spot _eur ) . filter ( v => v != null ) ;
const spot = spotVals . length ? spotVals . reduce ( ( a , b ) => a + b , 0 ) : null ;
out . push ( { fixed _eur : fixed , spot _eur : spot , label : chunk [ 0 ] . date . slice ( 5 ) } ) ;
}
return out ;
} ) ( ) ;
// Container-Breite messen und die SVG in echten Pixeln in genau dieser Breite
// zeichnen (keine viewBox-Skalierung mehr) — Höhe bleibt dabei fix, unabhängig
// von der Breite. Statt auf breiten Screens dieselben 14 Balken künstlich
// aufzublasen, wird stattdessen die Anzahl gezeigter Perioden an die Breite
// angepasst — mehr Platz zeigt mehr Wochen, bei einer angenehmen Zielbreite
// pro Balkenpaar (~46px), nicht überbreite Einzelbalken.
const H = 240 , PL = 40 , PR = 8 , PT = 14 , PB = 46 ;
const measuredW = el . clientWidth || 380 ;
const W = Math . max ( 300 , measuredW ) ;
const cW = W - PL - PR ;
const IDEAL _SLOT = 24 ; // px/Periode, entspricht dem Slot, der auf Handy-Breite gut aussah
const maxPeriods = Math . min ( 28 , Math . max ( 10 , Math . round ( cW / IDEAL _SLOT ) ) ) ;
const periods = allPeriods . slice ( - maxPeriods ) ;
const n = periods . length ;
const cH = H - PT - PB ;
const slot = cW / n ;
const barW = Math . max ( 3 , Math . min ( 26 , slot * 0.36 ) ) ;
const barGap = Math . max ( 1.5 , slot * 0.08 ) ;
const rBar = Math . min ( 2.5 , barW / 2 ) ;
const allVals = periods . flatMap ( d => [ d . fixed _eur , d . spot _eur ] . filter ( v => v != null ) ) ;
const maxVal = Math . max ( ... allVals , 0.01 ) ;
const yTicks = 4 ;
let gridLines = '' , yLabels = '' ;
for ( let i = 0 ; i <= yTicks ; i ++ ) {
const v = maxVal * i / yTicks ;
const y = PT + cH - ( cH * i / yTicks ) ;
gridLines += ` <line x1=" ${ PL } " y1=" ${ y . toFixed ( 1 ) } " x2=" ${ W - PR } " y2=" ${ y . toFixed ( 1 ) } " stroke=" ${ C . grid } " stroke-width="1" stroke-dasharray="2,3"/> ` ;
yLabels += ` <text x=" ${ PL - 6 } " y=" ${ ( y + 3 ) . toFixed ( 1 ) } " text-anchor="end" font-size="9" fill="#8b949e"> ${ fEur ( v ) } </text> ` ;
}
// Labels schräg statt horizontal — braucht bei 14 Balken pro Label deutlich
// weniger Breite, dadurch bleiben trotz Platzmangel alle 14 lesbar.
let bars = '' , xLabels = '' ;
periods . forEach ( ( d , i ) => {
const cx = PL + i * slot + slot / 2 ;
const hFixed = Math . max ( 0 , cH * d . fixed _eur / maxVal ) ;
bars += ` <rect x=" ${ ( cx - barGap / 2 - barW ) . toFixed ( 1 ) } " y=" ${ ( PT + cH - hFixed ) . toFixed ( 1 ) } " width=" ${ barW . toFixed ( 1 ) } " height=" ${ Math . max ( hFixed , 1 ) . toFixed ( 1 ) } " rx=" ${ rBar } " fill="url(#gradFixed)"/> ` ;
if ( d . spot _eur != null ) {
const hSpot = Math . max ( 0 , cH * d . spot _eur / maxVal ) ;
bars += ` <rect x=" ${ ( cx + barGap / 2 ) . toFixed ( 1 ) } " y=" ${ ( PT + cH - hSpot ) . toFixed ( 1 ) } " width=" ${ barW . toFixed ( 1 ) } " height=" ${ Math . max ( hSpot , 1 ) . toFixed ( 1 ) } " rx=" ${ rBar } " fill="url(#gradSpot)"/> ` ;
}
const ly = H - PB + 12 ;
xLabels += ` <text x=" ${ cx . toFixed ( 1 ) } " y=" ${ ly } " text-anchor="end" font-size="8.5" fill="#8b949e" transform="rotate(-40 ${ cx . toFixed ( 1 ) } ${ ly } )"> ${ d . label } </text> ` ;
} ) ;
const defs = ` <defs>
<linearGradient id="gradFixed" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color=" ${ C . fixed } " stop-opacity="0.95"/>
<stop offset="100%" stop-color=" ${ C . fixed } " stop-opacity="0.55"/>
</linearGradient>
<linearGradient id="gradSpot" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color=" ${ C . spot } " stop-opacity="0.95"/>
<stop offset="100%" stop-color=" ${ C . spot } " stop-opacity="0.55"/>
</linearGradient>
</defs> ` ;
const bucketLabel = bucket === 1 ? 'Tag' : bucket === 7 ? 'Woche' : '2 Wochen' ;
const rangeNote = allPeriods . length > n ? ` · letzte ${ n } ` : '' ;
const legend = ` <div style="display:flex;gap:18px;margin-top:10px;font-size:11px;color:var(--text-dim)">
<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background: ${ C . fixed } ;display:inline-block"></span>Festpreis</span>
<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background: ${ C . spot } ;display:inline-block"></span>Spot (hypothetisch)</span>
</div> ` ;
const chartSection = `
<div style="border-top:1px solid var(--border);padding-top:24px;margin-top:4px">
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:14px">Kosten je ${ bucketLabel } ${ rangeNote } </div>
<svg width=" ${ W } " height=" ${ H } " style="display:block">
${ defs } ${ gridLines } ${ yLabels } ${ bars } ${ xLabels }
</svg>
${ legend }
</div> ` ;
const chartSection = buildCostChart ( days , el , false ) ;
el . innerHTML = ` <div style="padding:4px 0"> ${ empfehlung } ${ cards } ${ chartSection } </div> ` ;
} catch ( e ) { el . innerHTML = '<div class="no-data">Fehler beim Laden</div>' ; console . error ( 'loadFinance:' , e ) ; }
@@ -1008,7 +1049,9 @@ function renderInverterList() {
</div>
</div>
<div class="inv-card-meta">
📡 ${ esc ( inv . modbus _ip ) } : ${ inv . modbus _port || 502 } · Slave ${ inv . modbus _address || 1 } <br>
${ inv . inverter _model === "GROWATT_WIFI_X"
? ` 🏠 HA-Entity: ${ esc ( inv . ha _entity _prefix || "– " ) } `
: ` 📡 ${ esc ( inv . modbus _ip ) } : ${ inv . modbus _port || 502 } · Slave ${ inv . modbus _address || 1 } ` } <br>
📨 ${ esc ( inv . mqtt _topic _prefix ) } <br>
⏱ alle ${ inv . update _interval || 30 } s
</div>
@@ -1021,16 +1064,19 @@ function renderInverterList() {
el . innerHTML = cards + ` <div class="add-btn" onclick="openModal()">+ Gerät hinzufügen</div> ` ;
}
function toggleEmsSection ( model ) {
function onModelChange ( model ) {
document . getElementById ( "ems-section" ) . style . display =
model === "KATHREIN_WALLBOX" ? "" : "none" ;
const isHaEntity = model === "GROWATT_WIFI_X" ;
document . getElementById ( "modbus-fields" ) . style . display = isHaEntity ? "none" : "" ;
document . getElementById ( "ha-entity-fields" ) . style . display = isHaEntity ? "" : "none" ;
}
async function openModal ( invId ) {
await loadModels ( ) ;
const modal = document . getElementById ( "modal-backdrop" ) ;
const modelSel = document . getElementById ( "modal-model" ) ;
modelSel . onchange = ( ) => toggleEmsSection ( modelSel . value ) ;
modelSel . onchange = ( ) => onModelChange ( modelSel . value ) ;
if ( invId ) {
const inv = invertersList . find ( i => i . id === invId ) || { } ;
document . getElementById ( "modal-title" ) . textContent = "Gerät bearbeiten" ;
@@ -1040,6 +1086,9 @@ async function openModal(invId) {
document . getElementById ( "modal-ip" ) . value = inv . modbus _ip || "" ;
document . getElementById ( "modal-port" ) . value = inv . modbus _port || 502 ;
document . getElementById ( "modal-addr" ) . value = inv . modbus _address || 1 ;
document . getElementById ( "modal-ha-prefix" ) . value = inv . ha _entity _prefix || "" ;
document . getElementById ( "modal-ha-url" ) . value = inv . ha _base _url || "http://localhost:8123" ;
document . getElementById ( "modal-ha-token" ) . value = inv . ha _token || "" ;
document . getElementById ( "modal-prefix" ) . value = inv . mqtt _topic _prefix || "" ;
document . getElementById ( "modal-interval" ) . value = inv . update _interval || 30 ;
document . getElementById ( "ems-min-pv" ) . value = inv . ems _min _pv ? ? 1400 ;
@@ -1055,6 +1104,9 @@ async function openModal(invId) {
document . getElementById ( "modal-ip" ) . value = "" ;
document . getElementById ( "modal-port" ) . value = "502" ;
document . getElementById ( "modal-addr" ) . value = "1" ;
document . getElementById ( "modal-ha-prefix" ) . value = "" ;
document . getElementById ( "modal-ha-url" ) . value = "http://localhost:8123" ;
document . getElementById ( "modal-ha-token" ) . value = "" ;
document . getElementById ( "modal-prefix" ) . value = "growatt/wechselrichter" + ( invertersList . length + 1 ) ;
document . getElementById ( "modal-interval" ) . value = "30" ;
document . getElementById ( "ems-min-pv" ) . value = "1400" ;
@@ -1063,7 +1115,7 @@ async function openModal(invId) {
document . getElementById ( "ems-phases" ) . value = "3" ;
document . getElementById ( "ems-max-current" ) . value = "32" ;
}
toggleEmsSection ( modelSel . value ) ;
onModelChange ( modelSel . value ) ;
modal . classList . add ( "open" ) ;
}
@@ -1091,6 +1143,11 @@ async function saveInverter() {
mqtt _topic _prefix : document . getElementById ( "modal-prefix" ) . value . trim ( ) ,
update _interval : parseInt ( document . getElementById ( "modal-interval" ) . value ) ,
} ;
if ( model === "GROWATT_WIFI_X" ) {
inv . ha _entity _prefix = document . getElementById ( "modal-ha-prefix" ) . value . trim ( ) ;
inv . ha _base _url = document . getElementById ( "modal-ha-url" ) . value . trim ( ) || "http://localhost:8123" ;
inv . ha _token = document . getElementById ( "modal-ha-token" ) . value . trim ( ) ;
}
if ( model === "KATHREIN_WALLBOX" ) {
inv . ems _min _pv = parseInt ( document . getElementById ( "ems-min-pv" ) . value ) ;
inv . ems _timeout = parseFloat ( document . getElementById ( "ems-timeout" ) . value ) ;