Skip to content

Commit 17afa7c

Browse files
authored
Bump version to 1.1.7 and clean up comments
## v1.1.7 ### Weather Widget - Widened border box from 82 to 86 and shifted divider/humidity icon +2px right for proper centering ### Battery Flow Line - Battery → Inverter discharge flow color changed from purple (var(--battery)) to yellow (var(--solar)) to match the battery watts display
1 parent c387abd commit 17afa7c

1 file changed

Lines changed: 6 additions & 49 deletions

File tree

‎xpower-flow-card.js‎

Lines changed: 6 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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.6';
4+
const V='1.1.7';
55

66
/* ═══════════════════════════════════════
77
xPower Flow Card — i18n
@@ -97,12 +97,6 @@ const LANG={
9797
charging:'\u0142adowanie',discharging:'roz\u0142adowanie',importing:'pobieranie',exporting:'oddawanie'}
9898
};
9999

100-
/* ═══════════════════════════════════════
101-
INVERTER PRESETS
102-
Convention after normalization:
103-
battery: positive = discharging, negative = charging
104-
grid: positive = import, negative = export
105-
═══════════════════════════════════════ */
106100
const PRESETS={
107101
deye:{
108102
label:'Deye (Solarman)',inverter_name:'DEYE',
@@ -201,9 +195,6 @@ const PRESETS={
201195
}
202196
};
203197

204-
/* ═══════════════════════════════════════
205-
DEFAULTS (Deye as base)
206-
═══════════════════════════════════════ */
207198
const DEFAULTS={
208199
preset:'deye',
209200
...PRESETS.deye.e,
@@ -214,7 +205,6 @@ const DEFAULTS={
214205
theme:'auto'
215206
};
216207

217-
/* Entity field definitions for editor */
218208
const ENTITY_FIELDS=[
219209
{key:'solar',label:'Solar Power'},{key:'battery',label:'Battery Power'},
220210
{key:'soc',label:'Battery SOC'},{key:'grid',label:'Grid Power'},
@@ -228,19 +218,13 @@ const ENTITY_FIELDS=[
228218
{key:'weather_temp',label:'Weather Temp.'},{key:'weather_humidity',label:'Weather Humidity'}
229219
];
230220

231-
/* ═══════════════════════════════════════
232-
CONSTANTS
233-
═══════════════════════════════════════ */
234221
const HIST_POINTS=48;
235222
const HIST_INTERVAL=5*60*1000;
236223
const RUNTIME_MIN_W=50;
237224
const ANIM_MAX_W=6000;
238225
const ANIM_MIN_SPD=1.5;
239226
const ANIM_MAX_SPD=3.5;
240227

241-
/* ═══════════════════════════════════════
242-
VISUAL EDITOR
243-
═══════════════════════════════════════ */
244228
class XPowerFlowCardEditor extends HTMLElement{
245229
constructor(){super();this._config={};this._hass=null;this._onchange=this._fire.bind(this);}
246230
_esc(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
@@ -260,7 +244,6 @@ class XPowerFlowCardEditor extends HTMLElement{
260244
cfg.shutdown_soc=isNaN(socVal)?20:socVal;
261245
const capVal=parseInt(this.querySelector('#ed-cap').value);
262246
cfg.battery_capacity=isNaN(capVal)?5120:capVal;
263-
// Preset change → apply preset entities and polarity
264247
const newPreset=this.querySelector('#ed-preset').value;
265248
if(newPreset!==cfg.preset&&PRESETS[newPreset]){
266249
const p=PRESETS[newPreset];
@@ -383,9 +366,6 @@ class XPowerFlowCardEditor extends HTMLElement{
383366
}
384367
customElements.define('xpower-flow-card-editor',XPowerFlowCardEditor);
385368

386-
/* ═══════════════════════════════════════
387-
MAIN CARD
388-
═══════════════════════════════════════ */
389369
class XPowerFlowCard extends HTMLElement{
390370
constructor(){super();this.attachShadow({mode:'open'});this._c={};this._h=null;this._prev={solar:0,bat:0,grid:0,load:0};this._hist={solar:[],load:[],grid:[]};this._fs={};this._spds={};this._histTimer=null;this._histLastLoad=0;this._histLoading=false;}
391371

@@ -409,7 +389,6 @@ disconnectedCallback(){
409389

410390
set hass(h){
411391
this._h=h;
412-
// Theme detection
413392
const t=this._c.theme||'auto';
414393
if(t==='auto'){const dark=h.themes?.darkMode!==false;this.classList.toggle('light',!dark);}
415394
else{this.classList.toggle('light',t==='light');}
@@ -418,7 +397,6 @@ set hass(h){
418397
this._update();
419398
}
420399

421-
/* ── Helpers ── */
422400
_gv(e){
423401
if(!e||!this._h||!this._h.states[e])return null;
424402
const s=this._h.states[e].state;
@@ -431,19 +409,15 @@ _fmt(v){if(v===null)return this._lang.unavailable;const a=Math.abs(v);return a>=
431409
_fmtE(v){if(v===null)return this._lang.unavailable;return v.toFixed(1)+' kWh';}
432410
_arrow(c,p){if(c===null)return '';const d=Math.abs(c)-Math.abs(p);return d>5?'\u25B4 ':d<-5?'\u25BE ':'\u25B8 ';}
433411

434-
/* ── Polarity normalization ──
435-
Normalizes to: battery positive=discharging, grid positive=import */
436412
_norm(raw,type){
437413
if(raw===null)return null;
438414
if(type==='bat'&&this._c.bat_polarity==='positive')return raw*-1;
439415
if(type==='grid'&&this._c.grid_polarity==='negative')return raw*-1;
440416
return raw;
441417
}
442418

443-
/* ── History ── */
444419
async _loadHistory(){if(this._histLoading)return;this._histLoading=true;try{const now=new Date();const start=new Date(now.getTime()-24*60*60*1000);const iso=encodeURIComponent(start.toISOString());const entities=encodeURIComponent([this._c.solar,this._c.load,this._c.grid].filter(Boolean).join(','));if(!entities)return;const url='history/period/'+iso+'?filter_entity_id='+entities+'&minimal_response&no_attributes&significant_changes_only';const res=await this._h.callApi('GET',url);if(!res||!res.length)return;const downsample=(arr,n)=>{if(!arr||!arr.length)return[];let src=arr;if(src.length>10000){const step=Math.ceil(src.length/5000);src=src.filter((_,i)=>i%step===0);}const out=[];for(let i=0;i<n;i++){const s=Math.floor(i*src.length/n);const e=Math.floor((i+1)*src.length/n);let sum=0,cnt=0;for(let j=s;j<e;j++){const v=parseFloat(src[j].state);if(!isNaN(v)){sum+=Math.abs(v);cnt++;}}if(cnt>0)out.push(sum/cnt);else out.push(0);}const sm=[];for(let i=0;i<out.length;i++){const p=i>0?out[i-1]:out[i];const nx=i<out.length-1?out[i+1]:out[i];sm.push((p+out[i]*2+nx)/4);}return sm;};for(const series of res){if(!series.length)continue;const eid=series[0].entity_id;const pts=downsample(series,HIST_POINTS);if(eid===this._c.solar)this._hist.solar=pts;else if(eid===this._c.load)this._hist.load=pts;else if(eid===this._c.grid)this._hist.grid=pts;}this._drawSparks();}catch(e){console.warn('xPower history:',e);}finally{this._histLoading=false;}}
445420

446-
/* ── SVG Render ── */
447421
_render(){const L=this._lang;const INV=this._c.inverter_name||'';const s=this.shadowRoot;s.innerHTML=`<style>
448422
:host{--solar:#FFB300;--battery:#7C4DFF;--grid:#42A5F5;--load:#26C6DA;--green:#66BB6A;--red:#EF5350;--orange:#FFA726;--t1:rgba(255,255,255,0.92);--t3:rgba(255,255,255,0.45)}
449423
:host(.light){--t1:rgba(0,0,0,0.85);--t3:rgba(0,0,0,0.45)}
@@ -491,12 +465,12 @@ svg{width:100%;height:auto;display:block}
491465
</style>
492466
<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)">
493467
<g id="wicons" style="display:none" transform="translate(-20,0)">
494-
<rect class="wb" x="-2" y="-4" width="82" height="20"/>
468+
<rect class="wb" x="-2" y="-4" width="86" height="20"/>
495469
<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>
496470
<text x="14" y="8" class="wt" id="wt"></text>
497-
<line id="wdiv" x1="42" y1="2" x2="42" y2="12" stroke="var(--t3)" stroke-width="0.5" opacity="0.4" style="display:none"/>
498-
<g id="wdrop" transform="translate(0,1)"><path d="M46,9 Q46,4 49,0 Q52,4 52,9 Q52,12 49,12 Q46,12 46,9Z" fill="none" stroke="var(--t3)" stroke-width="0.7"/><path d="M47.2,8.5 Q47.2,5.5 49,2 Q50.8,5.5 50.8,8.5 Q50.8,10.5 49,10.5 Q47.2,10.5 47.2,8.5Z" fill="var(--grid)" opacity="0.5"/></g>
499-
<text x="56" y="8" class="wt" id="wh"></text>
471+
<line id="wdiv" x1="44" y1="2" x2="44" y2="12" stroke="var(--t3)" stroke-width="0.5" opacity="0.4" style="display:none"/>
472+
<g id="wdrop" transform="translate(2,1)"><path d="M46,9 Q46,4 49,0 Q52,4 52,9 Q52,12 49,12 Q46,12 46,9Z" fill="none" stroke="var(--t3)" stroke-width="0.7"/><path d="M47.2,8.5 Q47.2,5.5 49,2 Q50.8,5.5 50.8,8.5 Q50.8,10.5 49,10.5 Q47.2,10.5 47.2,8.5Z" fill="var(--grid)" opacity="0.5"/></g>
473+
<text x="58" y="8" class="wt" id="wh"></text>
500474
</g>
501475
<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"/>
502476
<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"/>
@@ -513,7 +487,6 @@ svg{width:100%;height:auto;display:block}
513487
<div class="sb sc"><div class="sb-header"><span class="sl">${L.load24}</span><span class="sv" id="hx"></span></div><svg viewBox="0 0 200 40" preserveAspectRatio="none"><path id="hla"/><path id="hl"/><line class="cursor" id="cl" x1="0" y1="0" x2="0" y2="40"/><circle class="cursor-dot" id="dl2" cx="0" cy="0" r="3"/></svg><span class="sb-tip" id="tl"></span></div>
514488
</div></ha-card>`;this._setupTooltips();}
515489

516-
/* ── DOM helpers ── */
517490
_$(id){return this.shadowRoot.getElementById(id);}
518491
_setupTooltips(){
519492
const self=this;
@@ -550,11 +523,9 @@ _sf(el,id,p,d,c,o){if(Math.abs(p)<10){el.style.display='none';this._fs[id]=null;
550523
_spark(id,aid,data){const el=this._$(id);const af=this._$(aid);if(!el||!data.length)return;const w=200,h=40,py=2,max=Math.max(...data)||1;const pts=data.map((v,i)=>[(i/(data.length-1))*w,py+(1-v/max)*(h-py*2)]);if(pts.length<2)return;const tension=0.3;const cp=(p0,p1,p2,t)=>[p1[0]+(p2[0]-p0[0])*t,p1[1]+(p2[1]-p0[1])*t];let d='M'+pts[0][0].toFixed(1)+','+pts[0][1].toFixed(1);for(let i=0;i<pts.length-1;i++){const p0=pts[Math.max(0,i-1)];const p1=pts[i];const p2=pts[i+1];const p3=pts[Math.min(pts.length-1,i+2)];const c1=cp(p0,p1,p2,tension);const c2=[p2[0]-(p3[0]-p1[0])*tension,p2[1]-(p3[1]-p1[1])*tension];d+=' C'+c1[0].toFixed(1)+','+c1[1].toFixed(1)+' '+c2[0].toFixed(1)+','+c2[1].toFixed(1)+' '+p2[0].toFixed(1)+','+p2[1].toFixed(1);}el.setAttribute('d',d);if(af){af.setAttribute('d',d+'L'+w+','+h+'L0,'+h+'Z');}}
551524
_drawSparks(){this._spark('hs','hsa',this._hist.solar);this._spark('hl','hla',this._hist.load);this._spark('hg','hga',this._hist.grid);}
552525

553-
/* ── Main update ── */
554526
_update(){if(!this._h||!this.shadowRoot.getElementById('vs'))return;
555527
const c=this._c,L=this._lang;
556528

557-
// Read raw values and normalize polarity
558529
const sol=this._gv(c.solar);
559530
const bat=this._norm(this._gv(c.battery),'bat');
560531
const soc=this._gv(c.soc);
@@ -583,35 +554,26 @@ if(gv!==null&&freq!==null)this._$('gv').textContent=gv.toFixed(0)+'V \u00B7 '+fr
583554
if(bv!==null)this._$('bv').textContent=bv.toFixed(1)+'V';else this._$('bv').textContent='';
584555
const bt=this._gv(c.battery_temperature);if(bt!==null)this._$('bt').textContent=bt.toFixed(0)+'\u00B0C';else this._$('bt').textContent='';
585556

586-
587-
588557
const dS=this._gv(c.daily_solar)??0,dI=this._gv(c.daily_import)??0,dE=this._gv(c.daily_export)??0,dL=this._gv(c.daily_load)??0,dC=this._gv(c.daily_charge)??0,dD=this._gv(c.daily_discharge)??0;
589558
this._$('ds').textContent=L.daily+' '+this._fmtE(dS);
590559
this._$('dg').textContent=L.import_+' '+this._fmtE(dI)+' '+L.export_+' '+this._fmtE(dE);
591560
this._$('dl').textContent=L.daily+' '+this._fmtE(dL);
592561
this._$('db').textContent=L.charge+' '+this._fmtE(dC)+' '+L.discharge+' '+this._fmtE(dD);
593562

594-
// Flow animations (normalized: bat>0=discharging, grid>0=import)
595563
const solF=sol??0,batF=bat??0,gridF=grid??0,loadF=load??0;
596-
// Incoming flows (no delay): solar→inv, grid→inv, battery discharge→inv
597564
this._sf(this._$('fs'),'s',solF,'fd','var(--green)','0.8');
598565
if(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-
// Determine fastest incoming speed for sync delay
600566
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));
601567
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
568+
if(Math.abs(batF)>10){this._sf(this._$('fb'),'b',batF,batF<0?'fd':'fu',batF<0?'var(--green)':'var(--solar)','0.75');this._$('fb').style.animationDelay=batF<0?syncDelay.toFixed(2)+'s':'0s';}else{this._$('fb').style.display='none';}
605569
const solContrib=solF>0?solF:0;const batContrib=batF>0?batF:0;const gridContrib=gridF>0?gridF:0;
606570
let homeColor='var(--green)';
607571
if(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)';}
608572
this._sf(this._$('fh'),'h',loadF,'fr',homeColor,'0.75');
609573
this._$('fh').style.animationDelay=syncDelay>0?syncDelay.toFixed(2)+'s':'0s';
610-
// Incoming flows: no delay
611574
this._$('fs').style.animationDelay='0s';
612575
this._$('fg').style.animationDelay='0s';
613576

614-
// Battery runtime (with flicker threshold)
615577
const batCap=c.battery_capacity??5120;const shuSoc=c.shutdown_soc??20;
616578
if(batF>RUNTIME_MIN_W&&socVal>shuSoc){
617579
const remWh=(socVal-shuSoc)/100*batCap;const hrs=remWh/batF;
@@ -620,13 +582,11 @@ if(batF>RUNTIME_MIN_W&&socVal>shuSoc){
620582
this._$('br').textContent=h+'h '+pad(m)+'m \u25B8 '+shuSoc+'% @'+pad(eta.getHours())+':'+pad(eta.getMinutes());
621583
}else{this._$('br').textContent='';}
622584

623-
// Autarky
624585
const gridImp=gridF>0?gridF:0;
625586
const au=loadF>0?Math.max(0,Math.min(100,((loadF-gridImp)/loadF)*100)):0;
626587
this._$('va').textContent=L.autarky+': '+au.toFixed(0)+'%';
627588
const ap=this._$('ap');if(ap){let pc;if(au>=80)pc='#66BB6A';else if(au>=50)pc='#FFA726';else if(au>=25)pc='#FF7043';else pc='#EF5350';ap.setAttribute('fill',pc);}
628589

629-
// Weather
630590
const wtv=this._gv(c.weather_temp);const whv=this._gv(c.weather_humidity);
631591
const wicons=this._$('wicons');const wdrop=this._$('wdrop');const wdiv=this._$('wdiv');
632592
const wtEl=this._$('wt');const whEl=this._$('wh');
@@ -640,17 +600,14 @@ if(wtv!==null||whv!==null){
640600
if(wicons)wicons.style.display='none';
641601
}
642602

643-
// LEDs
644603
const _led=(id,on,color)=>{const el=this._$(id);if(!el)return;el.setAttribute('fill',on?color:'rgba(255,255,255,0.12)');if(on)el.setAttribute('class','led-on');else el.removeAttribute('class');};
645604
_led('led1',solF>10,'#66BB6A');
646605
_led('led2',batF>10,'#FFA726');
647606
_led('led3',gridF>10,'#EF5350');
648607
_led('led4',loadF>10,'#26C6DA');
649608

650-
// Bolt glow — only when receiving solar
651609
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');}}
652610

653-
// Sparkline values
654611
this._$('hv').textContent=this._fmt(sol)+' / '+this._fmtE(dS);
655612
this._$('hx').textContent=this._fmt(load)+' / '+this._fmtE(dL);
656613
this._$('hz').textContent=this._fmt(grid!==null?Math.abs(grid):null)+' / '+this._fmtE(dI+dE);

0 commit comments

Comments
 (0)