11// xPower Flow Card — Modern power flow card for solar hybrid inverters
22// Copyright (C) 2025 BTNBx
33// Licensed under GPL-3.0 — see LICENSE file
4- const V = '1.1.5 ' ;
4+ const V = '1.1.6 ' ;
55
66/* ═══════════════════════════════════════
77 xPower Flow Card — i18n
@@ -491,7 +491,7 @@ svg{width:100%;height:auto;display:block}
491491</style>
492492<ha-card><svg viewBox="0 0 526 520"><defs><filter id="glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur in="SourceGraphic" stdDeviation="2.5" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g transform="translate(25.5,10) scale(0.95)">
493493<g id="wicons" style="display:none" transform="translate(-20,0)">
494- <rect class="wb" x="-2" y="-4" width="72 " height="20"/>
494+ <rect class="wb" x="-2" y="-4" width="82 " height="20"/>
495495<g transform="translate(0,1)"><rect x="6" y="0" width="2.5" height="7" rx="1.2" fill="none" stroke="var(--t3)" stroke-width="0.7"/><circle cx="7.2" cy="9" r="2.5" fill="none" stroke="var(--t3)" stroke-width="0.7"/><line x1="7.2" y1="3" x2="7.2" y2="7" stroke="var(--red)" stroke-width="1" stroke-linecap="round"/><circle cx="7.2" cy="9" r="1.2" fill="var(--red)"/></g>
496496<text x="14" y="8" class="wt" id="wt"></text>
497497<line id="wdiv" x1="42" y1="2" x2="42" y2="12" stroke="var(--t3)" stroke-width="0.5" opacity="0.4" style="display:none"/>
@@ -501,7 +501,7 @@ svg{width:100%;height:auto;display:block}
501501<path class="fl" d="M250,96 L250,178"/><path class="fl" d="M250,272 L250,364"/><path class="fl" d="M90,225 L215,225"/><path class="fl" d="M285,225 L395,225"/>
502502<path id="fs" class="fa" d="M250,96 L250,178" pathLength="100" opacity="0"/><path id="fb" class="fa" d="M250,272 L250,364" pathLength="100" opacity="0"/><path id="fg" class="fa" d="M90,225 L215,225" pathLength="100" opacity="0"/><path id="fh" class="fa" d="M285,225 L395,225" pathLength="100" opacity="0"/>
503503<g><g transform="translate(250,38) scale(1.65) translate(-250,-38)"><circle cx="250" cy="38" r="9" fill="var(--solar)" opacity="0.85"/><line x1="250" y1="25" x2="250" y2="21" stroke="var(--solar)" stroke-width="2" stroke-linecap="round" opacity="0.6"/><line x1="250" y1="51" x2="250" y2="55" stroke="var(--solar)" stroke-width="2" stroke-linecap="round" opacity="0.6"/><line x1="237" y1="38" x2="233" y2="38" stroke="var(--solar)" stroke-width="2" stroke-linecap="round" opacity="0.6"/><line x1="263" y1="38" x2="267" y2="38" stroke="var(--solar)" stroke-width="2" stroke-linecap="round" opacity="0.6"/><line x1="240.8" y1="28.8" x2="238" y2="26" stroke="var(--solar)" stroke-width="2" stroke-linecap="round" opacity="0.5"/><line x1="259.2" y1="47.2" x2="262" y2="50" stroke="var(--solar)" stroke-width="2" stroke-linecap="round" opacity="0.5"/><line x1="259.2" y1="28.8" x2="262" y2="26" stroke="var(--solar)" stroke-width="2" stroke-linecap="round" opacity="0.5"/><line x1="240.8" y1="47.2" x2="238" y2="50" stroke="var(--solar)" stroke-width="2" stroke-linecap="round" opacity="0.5"/></g><text x="250" y="82" class="vm" style="fill:var(--green)" id="vs"></text><text x="250" y="-2" class="vl">${ L . solar } </text><text x="310" y="30" class="vc" id="ds" text-anchor="start"></text><text x="320" y="44" class="vc" id="pv" text-anchor="start"></text></g>
504- <g><g transform="translate(250,225) scale(1.65)"><rect x="-18" y="-24" width="36" height="48" rx="2" fill="rgba(255,255,255,0.08)" stroke="rgba(255,255,255,0.25)" stroke-width="1"/><rect x="-18" y="-24" width="36" height="5" rx="2" fill="rgba(255,255,255,0.12)"/><rect x="-12" y="-15" width="24" height="12" rx="1.5" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.2)" stroke-width="0.7"/><rect x="-8" y="-12" width="16" height="6" rx="1" fill="rgba(102,187,106,0.4)"/><circle id="led1" cx="-6" cy="2" r="1.2" fill="rgba(255,255,255,0.12)"/><circle id="led2" cx="-2" cy="2" r="1.2" fill="rgba(255,255,255,0.12)"/><circle id="led3" cx="2" cy="2" r="1.2" fill="rgba(255,255,255,0.12)"/><circle id="led4" cx="6" cy="2" r="1.2" fill="rgba(255,255,255,0.12)"/><path d="M-6,9 L-8,15 L-4,15 L-6,21" fill="var(--solar)" stroke="var(--solar)" stroke-width="0.8" opacity="0.9" filter="url(#glow)"/><rect x="2" y="12" width="10" height="3" rx="0.5" fill="rgba(255,255,255,0.1)"/><rect x="2" y="17" width="10" height="3" rx="0.5" fill="rgba(255,255,255,0.1)"/></g>${ INV ?'<text x="250" y="272" class="il">' + INV + '</text>' :'' } <text x="296" y="264" class="vc" id="tp" text-anchor="start"></text></g>
504+ <g><g transform="translate(250,225) scale(1.65)"><rect x="-18" y="-24" width="36" height="48" rx="2" fill="rgba(255,255,255,0.08)" stroke="rgba(255,255,255,0.25)" stroke-width="1"/><rect x="-18" y="-24" width="36" height="5" rx="2" fill="rgba(255,255,255,0.12)"/><rect x="-12" y="-15" width="24" height="12" rx="1.5" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.2)" stroke-width="0.7"/><rect x="-8" y="-12" width="16" height="6" rx="1" fill="rgba(102,187,106,0.4)"/><circle id="led1" cx="-6" cy="2" r="1.2" fill="rgba(255,255,255,0.12)"/><circle id="led2" cx="-2" cy="2" r="1.2" fill="rgba(255,255,255,0.12)"/><circle id="led3" cx="2" cy="2" r="1.2" fill="rgba(255,255,255,0.12)"/><circle id="led4" cx="6" cy="2" r="1.2" fill="rgba(255,255,255,0.12)"/><path id="bolt" d="M-6,9 L-8,15 L-4,15 L-6,21" fill="rgba(255,255,255,0.15)" stroke="rgba(255,255,255,0.3)" stroke-width="0.5"/><rect x="2" y="12" width="10" height="3" rx="0.5" fill="rgba(255,255,255,0.1)"/><rect x="2" y="17" width="10" height="3" rx="0.5" fill="rgba(255,255,255,0.1)"/></g>${ INV ?'<text x="250" y="272" class="il">' + INV + '</text>' :'' } <text x="296" y="264" class="vc" id="tp" text-anchor="start"></text></g>
505505<g><g transform="translate(66,225) scale(1.65) translate(-66,-196)"><rect x="64" y="181" width="4" height="30" rx="1" fill="var(--red)" opacity="0.7"/><rect x="54" y="183" width="24" height="3" rx="1" fill="var(--red)" opacity="0.6"/><rect x="57" y="192" width="18" height="2.5" rx="1" fill="var(--red)" opacity="0.5"/><path d="M60,211 L64,199 L68,199 L72,211" fill="var(--red)" opacity="0.4"/><circle cx="56" cy="184" r="1.5" fill="var(--red)" opacity="0.8"/><circle cx="76" cy="184" r="1.5" fill="var(--red)" opacity="0.8"/><circle cx="58" cy="193" r="1.2" fill="var(--red)" opacity="0.7"/><circle cx="74" cy="193" r="1.2" fill="var(--red)" opacity="0.7"/><line x1="54" y1="184" x2="46" y2="181" stroke="var(--red)" stroke-width="0.8" opacity="0.3"/><line x1="78" y1="184" x2="86" y2="181" stroke="var(--red)" stroke-width="0.8" opacity="0.3"/></g><text x="66" y="268" class="vm" style="fill:var(--red)" id="vg"></text><text x="66" y="190" class="vl">${ L . grid } </text><text x="66" y="286" class="vc" id="gv"></text><text x="66" y="300" class="vd" id="dg"></text></g>
506506<g><g transform="translate(434,225) scale(1.65) translate(-434,-188)"><path d="M416,188 L434,174 L452,188 Z" fill="var(--load)" opacity="0.8"/><rect x="420" y="187" width="28" height="18" rx="1" fill="var(--load)" opacity="0.6"/><rect x="430" y="195" width="8" height="10" rx="1" fill="rgba(0,0,0,0.3)"/><rect x="422" y="190" width="6" height="5" rx="0.5" fill="rgba(255,255,255,0.15)"/><rect x="440" y="190" width="6" height="5" rx="0.5" fill="rgba(255,255,255,0.15)"/><rect x="441" y="176" width="5" height="8" rx="1" fill="var(--load)" opacity="0.5"/></g><text x="434" y="268" class="vm" style="fill:var(--load)" id="vl"></text><text x="434" y="190" class="vl">${ L . load } </text><text x="434" y="288" class="vd" id="dl"></text></g>
507507<g><g transform="translate(250,400) scale(1.70) translate(-250,-351)"><rect x="232" y="341" width="32" height="20" rx="3" fill="var(--battery)" opacity="0.75"/><rect x="264" y="345.5" width="6" height="11" rx="2" fill="var(--battery)" opacity="0.9"/><rect x="235" y="344" width="26" height="14" rx="1.5" fill="rgba(0,0,0,0.35)"/><rect id="bl" x="235" y="344" width="26" height="14" rx="1.5" fill="var(--battery)" opacity="0.45"/></g><text x="250" y="440" class="vm" style="fill:var(--solar)" id="vb"></text><text x="250" y="462" class="vs" id="vc"></text><text x="250" y="372" class="vl">${ L . battery } </text><text x="310" y="394" class="vc" id="bv" text-anchor="start"></text><text x="310" y="406" class="vc" id="bt" text-anchor="start"></text><text x="250" y="483" class="vd" id="db"></text><text x="250" y="498" class="vc" id="br" style="fill:var(--t1)"></text></g>
@@ -593,14 +593,23 @@ this._$('db').textContent=L.charge+' '+this._fmtE(dC)+' '+L.discharge+' '+this._
593593
594594// Flow animations (normalized: bat>0=discharging, grid>0=import)
595595const solF = sol ?? 0 , batF = bat ?? 0 , gridF = grid ?? 0 , loadF = load ?? 0 ;
596+ // Incoming flows (no delay): solar→inv, grid→inv, battery discharge→inv
596597this . _sf ( this . _$ ( 'fs' ) , 's' , solF , 'fd' , 'var(--green)' , '0.8' ) ;
597- if ( Math . abs ( batF ) > 10 ) this . _sf ( this . _$ ( 'fb' ) , 'b' , batF , batF < 0 ?'fd' :'fu' , batF < 0 ?'var(--green)' :'var(--battery)' , '0.75' ) ; else this . _$ ( 'fb' ) . style . display = 'none' ;
598598if ( Math . abs ( gridF ) > 10 ) this . _sf ( this . _$ ( 'fg' ) , 'g' , gridF , gridF > 0 ?'fr' :'fL' , gridF > 0 ?'var(--red)' :'var(--green)' , '0.7' ) ; else this . _$ ( 'fg' ) . style . display = 'none' ;
599- // Home flow color: dominant source → green(solar), red(grid), purple(battery)
599+ // Determine fastest incoming speed for sync delay
600+ const inSpds = [ ] ; if ( solF > 10 ) inSpds . push ( this . _spd ( solF ) ) ; if ( gridF > 10 ) inSpds . push ( this . _spd ( Math . abs ( gridF ) ) ) ; if ( batF > 10 ) inSpds . push ( this . _spd ( batF ) ) ;
601+ const syncDelay = inSpds . length > 0 ?Math . min ( ...inSpds ) / 2 :0 ;
602+ // Battery: charging=outgoing(delay), discharging=incoming(no delay)
603+ if ( Math . abs ( batF ) > 10 ) { this . _sf ( this . _$ ( 'fb' ) , 'b' , batF , batF < 0 ?'fd' :'fu' , batF < 0 ?'var(--green)' :'var(--battery)' , '0.75' ) ; this . _$ ( 'fb' ) . style . animationDelay = batF < 0 ?syncDelay . toFixed ( 2 ) + 's' :'0s' ; } else { this . _$ ( 'fb' ) . style . display = 'none' ; }
604+ // Home flow: outgoing (delay), color by dominant source
600605const solContrib = solF > 0 ?solF :0 ; const batContrib = batF > 0 ?batF :0 ; const gridContrib = gridF > 0 ?gridF :0 ;
601606let homeColor = 'var(--green)' ;
602607if ( loadF > 10 ) { if ( gridContrib >= solContrib && gridContrib >= batContrib && gridContrib > 0 ) homeColor = 'var(--red)' ; else if ( batContrib >= solContrib && batContrib > 0 ) homeColor = 'var(--battery)' ; else homeColor = 'var(--green)' ; }
603608this . _sf ( this . _$ ( 'fh' ) , 'h' , loadF , 'fr' , homeColor , '0.75' ) ;
609+ this . _$ ( 'fh' ) . style . animationDelay = syncDelay > 0 ?syncDelay . toFixed ( 2 ) + 's' :'0s' ;
610+ // Incoming flows: no delay
611+ this . _$ ( 'fs' ) . style . animationDelay = '0s' ;
612+ this . _$ ( 'fg' ) . style . animationDelay = '0s' ;
604613
605614// Battery runtime (with flicker threshold)
606615const batCap = c . battery_capacity ?? 5120 ; const shuSoc = c . shutdown_soc ?? 20 ;
@@ -638,6 +647,9 @@ _led('led2',batF>10,'#FFA726');
638647_led ( 'led3' , gridF > 10 , '#EF5350' ) ;
639648_led ( 'led4' , loadF > 10 , '#26C6DA' ) ;
640649
650+ // Bolt glow — only when receiving solar
651+ const bolt = this . _$ ( 'bolt' ) ; if ( bolt ) { if ( solF > 10 ) { bolt . setAttribute ( 'fill' , 'var(--solar)' ) ; bolt . setAttribute ( 'stroke' , 'var(--solar)' ) ; bolt . setAttribute ( 'stroke-width' , '0.8' ) ; bolt . setAttribute ( 'opacity' , '0.9' ) ; bolt . setAttribute ( 'filter' , 'url(#glow)' ) ; } else { bolt . setAttribute ( 'fill' , 'rgba(255,255,255,0.15)' ) ; bolt . setAttribute ( 'stroke' , 'rgba(255,255,255,0.3)' ) ; bolt . setAttribute ( 'stroke-width' , '0.5' ) ; bolt . setAttribute ( 'opacity' , '1' ) ; bolt . removeAttribute ( 'filter' ) ; } }
652+
641653// Sparkline values
642654this . _$ ( 'hv' ) . textContent = this . _fmt ( sol ) + ' / ' + this . _fmtE ( dS ) ;
643655this . _$ ( 'hx' ) . textContent = this . _fmt ( load ) + ' / ' + this . _fmtE ( dL ) ;
0 commit comments