From 1dedfa09cc55bf491c8bd89a4932ec3536835284 Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:24:59 +0100 Subject: [PATCH 01/20] Update README-de.md --- docs/README-de.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README-de.md b/docs/README-de.md index 88e103c..f2adfe2 100644 --- a/docs/README-de.md +++ b/docs/README-de.md @@ -9,7 +9,7 @@ # Power Flux Card -Die ⚡Power Flux Card ist eine erweiterte, animierte Energiefluss-Karte für Home Assistant. Sie visualisiert die Energieverteilung zwischen Solar, Netz, Batterie und Verbrauchern mit wunderschönen Neon-Effekten und flüssigen Animationen. +Die ⚡Power Flux Card ist eine erweiterte, animierte Energiefluss-Karte für Home Assistant. Sie visualisiert die Energieverteilung zwischen Solar, Netz, Batterie und Verbrauchern mit wunderschönen Neon-Effekten und verschiedenen Animationen. power-flux-card power-flux-card power-flux-card @@ -48,7 +48,7 @@ Die ⚡Power Flux Card ist eine erweiterte, animierte Energiefluss-Karte für Ho 1. Lade die Datei `power-flux-card.js` von der [Releases](../../releases)-Seite herunter. 2. Lade sie in Ihren `www/community/power-flux-card/`-Ordner in Home Assistant hoch. 3. Füge die Ressource in Ihrer Dashboard-Konfiguration hinzu: - - URL: `/local/power-flux-card.js` + - URL: `/local/community/power-flux-card/power-flux-card.js` - Typ: JavaScript Module ### ⚙️ Konfiguration From f10b5c546b26fb583d0989f0034f8d3a6ed09bd1 Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:25:16 +0100 Subject: [PATCH 02/20] v_2.1 From a2dab3d45b2d6e7cf6660f34ee4d263ce125b4ff Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:26:24 +0100 Subject: [PATCH 03/20] v_2.1 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60fde5a..74f36c0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ # Power Flux Card -The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assistant. It visualizes the power distribution between Solar, Grid, Battery, and Consumers with beautiful neon effects and smooth animations. +The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assistant. It visualizes the power distribution between Solar, Grid, Battery, and Consumers with beautiful neon effects and diffrent animations. power-flux-card power-flux-card power-flux-card @@ -49,7 +49,7 @@ The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assis 1. Download `power-flux-card.js` from the [Releases](../../releases) page. 2. Upload it to `www/community/power-flux-card/` folder in Home Assistant. 3. Add the resource in your Dashboard configuration: - - URL: `/local/power-flux-card.js` + - URL: `/local/community/power-flux-card/power-flux-card.js` - Type: JavaScript Module ### ⚙️ Configuration From b742bcd981c443612f9863a3061ff0a5f80b3a83 Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:35:26 +0100 Subject: [PATCH 04/20] v_2.1 --- docs/images/power-flux-card-compact.jpg | Bin 12185 -> 3311 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/power-flux-card-compact.jpg b/docs/images/power-flux-card-compact.jpg index 1c9f0430a43e9b76a459134cf4e033c3895d980a..dd12e2b8e3aff99b228eb2d5c42271efc867cc98 100644 GIT binary patch literal 3311 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 Jbd3?3@qb35lRf|d literal 12185 zcmeHtc|4Tu_y28+EKL|>$s}Z%q-@zIl~7qGAxkEeJ;~Zwr%-CdSdt<}G%0IY5-HiY z%1$Wz7GcIR2D5x`eV^y~JoS8DuTQ`4U%$`m)tHNy`<`=M=Y7umoa>zH9>yoeFtGjj zF{5Ju6B83~4*UTa1Hci0g_(Kt4Zc{xHyZ~V8!IatCp-HV4sK3vZZ1wPE}m_C+j+L} zZsX$GF1VeSA1WXqzzq@FAqd^U2Ni&BPQt_j%CNF+WnBIi{y)DM?*Lv7AeNbr zg=sgy%*({W%fzS!U;x0x25P&h@ECj1>>OJ;xxf#UYzLT`SXh`@SvIu>KOF+z z2UvO8_#_VKZQ(~cudGD~ToPt6-ghizH$jHjctEj3S zRM*fva#a7AfuYfH^V1fVR%fhjE?jhUa&~cb^YQibzlshB43D^RGxFALY<$Aq#Cu8i zA0%gFJ|;ZLdipH8u&B7?W$CN3^6Hw}y84F3rsj^$uI`@RPksF(qhsR}lT*`QXGn`n z%PZtn%G&xSE+&BGe_{QF>?d5jATDNBRu)$FOyM=z?jN-Ccs3Y_*F*MNonpOF0p>|eP00d5v1(0MGp00N+# z!XDE)=C2E$Rr%L#s?xKx zmA~oneOCNNkc*dgtu%?$pKdsuyrZeis)4ENdTc-o4rvE{aXhog5^=Ce{bQPk{plPt z-&5X2ckfla@whmbS=9eEuTwi7E1SQxd@Tbj3;c7ehYx#RSIv_jY_73>+2)(K$7SGD z+N1+(e>glNz^!H9!rpC=FFQ?rkaFHRcN1HarVGwehDIQKbwy{oeQA6<_6}|6+C~dx ziVpY`s=RnqeL&22C%o}lmYIS8n-Q8Ld@Zs&To`pRMm2uY!jkPOeVvsZ zChIKukaw!PkCI^aGGQbs?3+Xx*60!5EDepULiY_yi(VUdJinvW4BpsKGY}-tW3QY@ zn9h{5UONcIA4@rFf;CM_K6|Qj!7zm`OeN4Qdh-q=l!PeXGI%(tGL0s~A8nk^yj)*5j8&7(mk*1O+?tG;;&4 z2;N6yh7T}+UE~$a1KPKF%!ogwlOB#*gR?ULUIXgBArVZQI|GnD#{fp*H#2bGax3ZA zM;O4h25R^~H{E%)sEh&J7#K5M=fRBU!l?Q%;_F~n2B23!O$=uMxAH}>h{8e!u+APy zXMrt3JgwG-20u>y2RKbXz?u3rI5I!NN&N>L)X~ru1YfXK9_2#4kkYK0mtFr%vTV#0 z@+GRM_Rgx-`!Jn|XPnB73;>49ArMiucqmol-i#^tY;V8lm+aihkLSb8$_8FttvAt8 z3UPne`pO6Lz;R{&CmWS9wb4#5qoBe&X-)G9wmKhk)%N*jKl^B$7r1t212wvUEx|*A zH7HR*S#EDdoNyXmq6O)rG!O0iO|!g2Jv@07;zRU;jZ3!-Q1{Gx(v z%}+Y1_9XkJ7%6tYr91olL;}PQ-nLT}SgGu@#?V-57yuJ)O-y&NU zD_uxhViE&r(?$??Fo4*gz{>Y@@>Rr-I5K~5xIhikfNY6k0EdVS;N1nf-rNiW(Awzx z%mDsQY}}6&w&Kx;^=bU29(!pHbz$K@VuE5WPB4IF2!y%?F)j`ws{9p12|ptGNGAs- zMTE-Ch%1t|rKY3b28Ea&>LP524f;LSn>$G`vpd%OC~-{d%(+GAvyodWROHj4VEH#u zHG$_!iz{D-sdeQ?@HXu&D!iIFk&$kOdEQp)TAf|a6A7LR_7rik)3SM zLJvOjogq6>R0RYS(`F7Ph*2;>xcoSQ_g&<-5$aP!ArZDT`aL&+ZvH_HgCFJH?`Ghf zArpGEq-zWSzx_Km46ObG5a=cj$hQi1D&>2M(RyP(b~S3{fOE82Ur8<(^9g4@eTn+e zkY*;HR%8rNU`D$iD&;Z=>(qAj%qM(lNOj#u2=bAxk|jOq{v@Q`I47}Iln@J_Iv!{& z<-R+!aBoye^}{W~?Cf0^Syq?}pG8It4=3V8l}2*6I9y9ZamafmSb5nM7@-Jn?VTN- z2HTQ$exbc5uNpbB78W&lh{?9_#Ty>7HrlmK$w=GB;wKA$j>syCT+I9wV{r|OJS~y6 zYPI$s&1z1*LO+f87i7&!icKgOm+4vb6Lny+Z~w_;6SA3oHEU)V$6>@&?e{Q^>jp!u zlVm_+LoC`W()ZwgoS;`uU;HDiIebHri2>+Jd{6f=1$03?7BMXc?m;9BNd@zF`Lg4( zn>xYz(2zCK_fYf0fb=mzd+1Bqf0cf`FIR^{R!_XXJkJIW<3{_#gunJ!w%g6VICjrW z#bK-{R#hC5~gUJOLK|gVJ4d^Pz(1Jx%-0$4~Z5A(h@0GLyEr9KUCu zE~cj3yM<7-r_ADZ3}=i;iROJ5*~_W%R+d2n*{;^h-F}X0lt-gi&o`Ji$;i;eL4mYnNa++bU*tP8Y_n3dvJIVXaopaUZZP%qsm*y`M(t7Hw6*ByrYjX$g zE2hgN6AniwUN3q~`VgIakgOSfhs)hD$42_wl|BBowZ+~U44_at=8(=7HXpwZbl9S3 z0SCoWC0RqJ6gw#=X~`BF@P0O~%Q-+&^+UJB0oeS(j@+tMHj9M$gKgbUb%*nI9o?*B zy8cPp633Ub(Vw>eAg?X|E5lrNF-7MhhSsAD;IK zGUs2o;~ydLr8I=k-lI=43vw8p{DNTWkH$rY!YG;byv50on=2ugRn?t3bERJa8+XlZ zQN#>gX-vB|2}R$!4@1GyB0Gx*jCa?I!nw{|${jX~vmL0Hj8Wam{B#)NUoRF@22ds4`n^Pv#ob0Nn`(n;BJ2d7 zzPs|mbS`F8brQDB4xw`DV{(csr^YtoC}7RPDLHCI<8KvU0K@T9U~Q85kJ(T!mcfk2 zTd*IDC3Vd3DaeX{5|xVaMu;Isjs_oR02u+Jl~g`E2C!L!-J&EifTK%w7>X2F1dYPU zx>&?tE9=qoJHRA{n>QGXr2eEMj)H{oRZgd&3m5=v5`wB9+R$!5JyuaezXqy#-4M~; zk6I$p8mU*ngvv{T({E2<8354;+`l&qxu0jD9673`UE``Q3i{F z{bc!nfDqV#>BC4OOOSPzNIKgt24LliX>C}Q{6#}&(k1O3z+D_v^W9_E%$(vHg+3mt z{twG+mJC0^u=*AP#nigLM@(;nQJ~?pvjhf!fPKa_Kz_E(&md^H3>+moAdhKk<@@#-h+;00Bj&#NCF#L_R=T<(fQVneA)GemydP?>DpSChF-o5ZbS%f zlGZIZBCLy^XKX>HcTjsR$Hzp{HJ>X25BGCyqmM7w&Y*QGs*njBIUR`wSYFv3v z5Z0~KSNuJ-Bct5&wk}t~$W)A~C!|RcPu4hO6etvZSCfC*|9Rue2Orgr)D@*qm+)S1 zZ|3R!(oPLu_M~sE;)8@GFJC|8PvUO2Eo1H4u@8fi8dr|GKdU zDvDCd0HkUeKm+9c`rarSDNuQ)TsEUJ(zLxAE1S;z(}1KVjP*tZE0QoZgWM#6sjwB= z1si(DlD71Sk&|Xx()d70_sg$Sepr-})M;Hen~4d_vyJ)3T5sqophxKPM#HIrH8pd2 zL+GQF>`tTWQ!kTF#F|KK2y`)kw=sSSyO3TS?E8j6cZ1>5Duf^v-lHlyXI`(V zh|TxQ$*Sm8wXV=n5qXlaapo|Xf;`Y;F}o~IsYL9~h|MNMSxukGejxKAB}-OA)}~q6 zu46JWIry2r?BbVT(a%KwqY$LCj_JV=sdx@LAn_*fUT`mWkX5c3mv zN5oA^NF%j$-tV_lI~?C0YqV_*?MBrmQE<8ST`2gCOqtGjBe&#It!Ft_z8yW}i(^>w zmGMw=X?I{j^N?~o#<}u#a#B5v6-6nWG*D+2$@3Cd&s|~j z_apV)C3YVuA%t5J-mhuu+HRCCl@v@~Z8twXnjLJ}+xrx$FEE*W1ByJv_V=tIqz{v7r?J|rI$aVjQQ!F@C( z1b(PPTc1WKHK{mXT7JngPNmW{c|t0F@aRS3_n8j3kB3AK#khZRZzzU@i%S)f{7VYl z)1=z8O?6A7Y!~Jr+YGIJsY?!9JUR8Z9TbClydQYH#37KY;Ww#^lSK=Nw2F1uZQApWdqM2}D>xKc zx0tIJ+Z1b^nDCZ}xu&WhQ`g^s4b}t8O%Hb~GV#-6f6EV!jvns3PN&ak_iBvsou*5e3n<*(5_RypNyBq{8%S^uzOM_*xD@C4 zb`Qm5sn+LBM7hOa4mw%o<$)GaBM0aQsHsv@W89s~Cj0kWUGrw=Euc50?Ia1^0uNn! z`H#j^K7Q14;S+U=_MJK#vltuCbRg)~qx<7>(i~Eem)23$3z1|LmoCi3Uf?-KjYsYC zu}+7`R5@uv0^9xkHQ|BFhN92anQxb0)}3bnz0?E3A0;Da&LKuL)$%6{Bv^3opmlibh2% zTa%)@SHnCn?|xO$MtFYR(Mt@syK3iq2Vn_?@hSO^^ZTSk?{~{pI`arZU}&w{Kqo@> znW}U<`?E$D)Cd87CdYkQ_E=7V6e29`Xj1d~tMz)MRlTLiJJ#_IK zH4jkAkwZeUc5>WXopvzAoTEq*g$yV#Fgqg1c5gA{kLj2BC;T>a3a1)huBDc)1&!0D z*h+oc%-CBhOJu0`z68-)JZZc;1M~yG7o*ap_F&E!06Q}Sd*oRn1^GdtzG+o-+N}$A z#ll*5EQH4kV9cKYS^Q zF0KQ%nAbwV!r>j*K^tcN^TJ)3-bsAmx7Oq6!a?Ci!)Ko!Lz`9m^HAB{l|rf zho;n6aN0J^<>8IWSHGwyVwBGdu?QY9vgpxXhtJc;mi@6*@O&{6x7mWH!)_pmd|>|` z54M&6=F%-Akd#djDi8Z_j6XZ}zuS)_`8CAP+|0M7i@JjyFDG%8&e952yE!W#nT8(@ zMZPUrnXU^mIbmZ(2`E_0Tx+eP;)LrlX7nbb4VdeP_iLew&hA$!*5nTYq=Qto-WbfZ zz{moX9qepz;28@~$ryf}4IKn>8>HG;_xcdoFSKO=@vw4KfF$#soCX6bR<%RK0ecOQ zlva`8<5~<|$KdCwXsM{ejT*WHiVw3Hcp*jl!N&iXz6(?1fu5pYw-*P4B>?PJR$zVI z8;TW(-;4d>k^fFJs(!B-tm;ueu>{$);ffx#Z0cC%9uu?uPTL$vSe{962jb|k?v{`0 z7|j^1ojab&Ho3xIzbV#j&UmZD0B(LHmC~4(A^#$2D%i5JO~WV<1OpH+Fz+*@Cr%mk z756zB?hbJDi%~iCe*Ow{+vBwA%}2pMidzYc4dLiYl-o{Zn;YvkuC1%zzVSCl)9#NK zLCzbJpFBJ9w5;8HEGcSlg$K#hzPos8Y463j-8W4>N=9G!taJO6!3%+Eq`n`)@q*yI zn(3>dBbMnKVcCD~w3e~Ej}{~K-SjMHDWMq;Ov~x8r%x=EKDS>I_>4|5E^e zEV55fofBTk03r^)!!IK-i;)NlxXE&OUPnxWr|T2jFr}av$*gEA+h?Ilf-Gh zE9nW}c$}5)p3micuD)wUV1sQ#hRRMBWUsK_dj8{^>9lEl#$_hxn;KYV9)v36UNT4wOn zggP)5ekS`_bKRn=&r&jW0X#qDAmhvR?~Q4Iwu-YrRjhB=^1;M1Y0A851o%^ z^~>Ko`}=p)q?i&q;_k~fsSBL4PQLmnRjWf-N+ZpCs9Ig%N%)TJp64807U^)897(l# z1t`+e$mZ+8dVMq7Ord7VSWn3V9}7aU)r_)r^TAKW%@|KpGG-wgQOr-nZ_E(Xw$l1d zpUV)Oo14oe>1}_%hbObr=V0ed=i8gBr=7`Rm?8!cf~2M6zcGNJ(+uFb%&N-24b}v@eN#4_Z|LAX5x*u);}aN zH}nvF;4$el2C%u}=F(!h`)>Xr0_)#OZ>6o6ZZ3!0TzA63SNRW#r+;XT*<3DUb6q&s zBmDmg82q8NIel}v&2=@-oaO!v^S3wukEBhzyGdyB@^_X$QKbi7APPUL3ky7b+bM^t t0^XBjy*IuTtFzuc>#ny$?DSU-0r@ Date: Thu, 19 Feb 2026 23:36:38 +0100 Subject: [PATCH 05/20] v_2.1 --- docs/images/power-flux-card-compact.jpg | Bin 12185 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/images/power-flux-card-compact.jpg diff --git a/docs/images/power-flux-card-compact.jpg b/docs/images/power-flux-card-compact.jpg deleted file mode 100644 index 1c9f0430a43e9b76a459134cf4e033c3895d980a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12185 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 t0^XBjy*IuTtFzuc>#ny$?DSU-0r@ Date: Thu, 19 Feb 2026 23:40:24 +0100 Subject: [PATCH 06/20] v_2.1 --- docs/images/power-flux-card-compact.jpg | Bin 0 -> 6390 bytes docs/images/power-flux-card-compact2.jpg | Bin 0 -> 7232 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/power-flux-card-compact.jpg create mode 100644 docs/images/power-flux-card-compact2.jpg diff --git a/docs/images/power-flux-card-compact.jpg b/docs/images/power-flux-card-compact.jpg new file mode 100644 index 0000000000000000000000000000000000000000..19fafd049c6f679a4261c81559cd87c455a5ee8a GIT binary patch literal 6390 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 PgU)=#y9fZmheLk@;G4!g literal 0 HcmV?d00001 diff --git a/docs/images/power-flux-card-compact2.jpg b/docs/images/power-flux-card-compact2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..04dc763773f2a2c78fdd39171219ab05cf86bc17 GIT binary patch literal 7232 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 Date: Thu, 19 Feb 2026 23:41:19 +0100 Subject: [PATCH 07/20] v_2.1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74f36c0..0ecbb8c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assistant. It visualizes the power distribution between Solar, Grid, Battery, and Consumers with beautiful neon effects and diffrent animations. power-flux-card power-flux-card -power-flux-card +power-flux-card power-flux-card ### ✨ Features From a163f4c86ee74ea8601a3898b9c39c7ef00310a8 Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:41:42 +0100 Subject: [PATCH 08/20] v_2.1 --- docs/README-de.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README-de.md b/docs/README-de.md index f2adfe2..c0a747f 100644 --- a/docs/README-de.md +++ b/docs/README-de.md @@ -12,7 +12,7 @@ Die ⚡Power Flux Card ist eine erweiterte, animierte Energiefluss-Karte für Home Assistant. Sie visualisiert die Energieverteilung zwischen Solar, Netz, Batterie und Verbrauchern mit wunderschönen Neon-Effekten und verschiedenen Animationen. power-flux-card power-flux-card -power-flux-card +power-flux-card power-flux-card ### ✨ Funktionen From e897990d6a118d3cebeffcd5b5e07f1ac86e86ce Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Sat, 21 Feb 2026 02:46:06 +0100 Subject: [PATCH 09/20] v_2.1 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0ecbb8c..b331d24 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assistant. It visualizes the power distribution between Solar, Grid, Battery, and Consumers with beautiful neon effects and diffrent animations. +If you like the Card, I would appreciate a Star rating ⭐ from you. 🤗 + power-flux-card power-flux-card power-flux-card power-flux-card From 7f6e44e938989f87f72cea8ac4d1b00aab819eb5 Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Sat, 21 Feb 2026 02:46:59 +0100 Subject: [PATCH 10/20] v_2.1 --- docs/README-de.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/README-de.md b/docs/README-de.md index c0a747f..96f99ef 100644 --- a/docs/README-de.md +++ b/docs/README-de.md @@ -11,6 +11,8 @@ Die ⚡Power Flux Card ist eine erweiterte, animierte Energiefluss-Karte für Home Assistant. Sie visualisiert die Energieverteilung zwischen Solar, Netz, Batterie und Verbrauchern mit wunderschönen Neon-Effekten und verschiedenen Animationen. +Wenn euch die custom Card gefällt, würde ich mich sehr über eine Sternebewertung ⭐ freuen. 🤗 + power-flux-card power-flux-card power-flux-card power-flux-card From e2f070941d8ca3cf9038d810f42d6be02b23a9da Mon Sep 17 00:00:00 2001 From: jayjojayson Date: Tue, 24 Feb 2026 00:29:01 +0100 Subject: [PATCH 11/20] v_2.2 --- README.md | 12 ++ docs/README-de.md | 15 ++ src/lang-de.js | 8 + src/lang-en.js | 8 + src/power-flux-card-editor.js | 256 ++++++++++++++++++-------- src/power-flux-card.js | 336 +++++++++++++++++++++++++--------- 6 files changed, 471 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index b331d24..66a0584 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,19 @@ If you like the Card, I would appreciate a Star rating ⭐ from you. 🤗 - **Donut Chart**: Optional donut chart around the house icon showing energy mix. - **Comet Tail / Dashed Lines**: Choose your preferred animation style. - **Zoom**: Adjustable scale to fit your dashboard. + - **Custom Colors**: Define custom colors for each source and consumer via the editor. + - **Background Color**: Enable a slightly tinted background for the circles in the default view. +- **More Info**: Click on any source/consumer for detailed information in a more-info dialog. +- **Grid Import/Export**: Supports both separate Import/Export entities or a combined entity with positive/negative values. +- **Grid-to-Battery**: Optional direct sensor for Grid-to-Battery flow, bypassing the standard calculation. +- **Secondary Sensors**: Optionally display a secondary sensor value in the main circles (e.g., daily yield for Solar, current charge/discharge power for Battery) and consumer bubbles. - **Localization**: Fully translated in English and German. - **Visual Editor**: easy configuration via the Home Assistant UI. +[![Support](https://img.shields.io/badge/Features-Video%20german-steelblue?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=HGFBJJRWGW0) + +--- + ### 🚀 Installation ### HACS (Recommended) @@ -54,6 +64,8 @@ If you like the Card, I would appreciate a Star rating ⭐ from you. 🤗 - URL: `/local/community/power-flux-card/power-flux-card.js` - Type: JavaScript Module +--- + ### ⚙️ Configuration You can configure the card directly via the visual editor in Home Assistant. diff --git a/docs/README-de.md b/docs/README-de.md index 96f99ef..3e90900 100644 --- a/docs/README-de.md +++ b/docs/README-de.md @@ -26,9 +26,21 @@ Wenn euch die custom Card gefällt, würde ich mich sehr über eine Sternebewert - **Donut Chart**: Optionales Donut-Diagramm um das Haus-Icon, das den Energiemix zeigt. - **Kometenschweif / Gestrichelte Linien**: Wählen Sie Ihren bevorzugten Animationsstil. - **Zoom**: Anpassbare Größe für Ihr Dashboard. + - **Benutzerdefinierte Farben**: Definiere benutzerdefinierte Farben für jede Quelle und jeden Verbraucher über den Editor. + - **Hintergrundfarbe**: Aktiviere einen leicht getönten Hintergrund für die Kreise in der Standard-Ansicht. +- **Dynamische Animationsgeschwindigkeit**: Partikelgeschwindigkeit und -dichte passen sich dem aktuellen Energiefluss an. +- **Weitere Informationen**: Klicke auf eine beliebige Quelle/Verbraucher, um detaillierte Informationen in einem More-Info-Dialog anzuzeigen. +- **Netz-Import/Export**: Unterstützt sowohl separate Import/Export-Entitäten als auch eine kombinierte Entität mit positiven/negativen Werten. +- **Netz-zu-Batterie**: Optionaler direkter Sensor für den Netz-zu-Batterie-Fluss, der die Standardberechnung umgeht. +- **Sekundäre Sensoren**: Optional können sekundäre Sensorwerte in den Hauptkreisen (z.B. Tagesertrag für Solar, aktuelle Lade-/Entladeleistung für Batterie) angezeigt werden. - **Lokalisierung**: Vollständig übersetzt in Deutsch und Englisch. - **Visueller Editor**: Einfache Konfiguration über die Home Assistant UI. +[![Watch the video](https://img.youtube.com/vi/HGFBJJRWGW0/0.jpg)](https://www.youtube.com/watch?v=HGFBJJRWGW0 +) + +--- + ### 🚀 Installation ### HACS (Empfohlen) @@ -53,6 +65,9 @@ Wenn euch die custom Card gefällt, würde ich mich sehr über eine Sternebewert - URL: `/local/community/power-flux-card/power-flux-card.js` - Typ: JavaScript Module + +--- + ### ⚙️ Konfiguration Du kannst die Karte direkt über den visuellen Editor in Home Assistant konfigurieren. diff --git a/src/lang-de.js b/src/lang-de.js index bb15bc2..f7220c7 100644 --- a/src/lang-de.js +++ b/src/lang-de.js @@ -29,8 +29,16 @@ export default { "editor.donut_chart": "Donut Chart (Grid/Haus)", "editor.comet_tail": "Comet Tail Effect", "editor.dashed_line": "Dashed Line Effect", + "editor.tinted_background": "Farbiger Hintergrund in Kreisen", "editor.colored_values": "Farbige Textwerte", "editor.hide_consumer_icons": "Icons unten ausblenden", + "editor.invert_consumer_1": "Sensorwert invertieren (+/-)", + "editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)", + "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)", + "editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", + "editor.grid_combined_sensor": "Kombinierter Netz-Sensor (W, Optional)", + "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt die getrennten Import/Export Sensoren.", + "editor.color_picker": "Farbe anpassen", }, card: { "card.label_solar": "Solar", diff --git a/src/lang-en.js b/src/lang-en.js index 774f660..f955cb9 100644 --- a/src/lang-en.js +++ b/src/lang-en.js @@ -29,8 +29,16 @@ export default { "editor.donut_chart": "Donut Chart (Grid/House)", "editor.comet_tail": "Comet Tail Effect", "editor.dashed_line": "Dashed Line Effect", + "editor.tinted_background": "Tinted Background in Bubbles", "editor.colored_values": "Colored Text Values", "editor.hide_consumer_icons": "Hide Consumer Icons", + "editor.invert_consumer_1": "Invert Sensor Value (+/-)", + "editor.secondary_sensor": "Secondary Sensor (display only)", + "editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)", + "editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", + "editor.grid_combined_sensor": "Combined Grid Sensor (W, Optional)", + "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides separate import/export sensors.", + "editor.color_picker": "Custom Color", }, card: { "card.label_solar": "Solar", diff --git a/src/power-flux-card-editor.js b/src/power-flux-card-editor.js index fc488e0..bf1177a 100644 --- a/src/power-flux-card-editor.js +++ b/src/power-flux-card-editor.js @@ -65,10 +65,12 @@ class PowerFluxCardEditor extends LitElement { if (key) { const entityKeys = [ - 'solar', 'grid', 'grid_export', - 'battery', 'battery_soc', + 'solar', 'grid', 'grid_export', 'grid_combined', + 'battery', 'battery_soc', 'grid_to_battery', 'house', - 'consumer_1', 'consumer_2', 'consumer_3' + 'consumer_1', 'consumer_2', 'consumer_3', + 'secondary_solar', 'secondary_grid', 'secondary_battery', + 'secondary_consumer_1', 'secondary_consumer_2', 'secondary_consumer_3' ]; let newConfig = { ...this._config }; @@ -101,6 +103,65 @@ class PowerFluxCardEditor extends LitElement { this._subView = null; } + _clearEntity(key) { + const newConfig = { ...this._config }; + const currentEntities = newConfig.entities || {}; + const newEntities = { ...currentEntities, [key]: "" }; + newConfig.entities = newEntities; + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } + + _colorChanged(key, ev) { + const newConfig = { ...this._config, [key]: ev.target.value }; + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } + + _resetColor(key) { + const newConfig = { ...this._config }; + delete newConfig[key]; + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } + + _renderEntitySelector(entitySelectorSchema, value, configValue, label) { + const val = value || ""; + return html` +
+ + ${val ? html` this._clearEntity(configValue)} + >` : ''} +
+ `; + } + + _renderColorPicker(key, label, defaultColor) { + const currentColor = this._config[key] || defaultColor; + const hasCustom = !!this._config[key]; + return html` +
+ this._colorChanged(key, e)}> + ${label} + ${hasCustom ? html` this._resetColor(key)}>` : ''} +
+ `; + } + static get styles() { return css` .card-config { @@ -179,6 +240,60 @@ class PowerFluxCardEditor extends LitElement { border-bottom: 1px solid var(--divider-color); margin: 10px 0; } + .entity-picker-wrapper { + position: relative; + display: flex; + align-items: center; + gap: 4px; + } + .entity-picker-wrapper ha-selector { + flex: 1; + } + .clear-entity-btn { + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + cursor: pointer; + flex-shrink: 0; + margin-top: -12px; + } + .clear-entity-btn:hover { + color: var(--error-color, #db4437); + } + .color-picker-row { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 0; + } + .color-picker-row input[type="color"] { + -webkit-appearance: none; + border: 2px solid var(--divider-color); + border-radius: 50%; + width: 36px; + height: 36px; + padding: 2px; + cursor: pointer; + background: transparent; + } + .color-picker-row input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; + } + .color-picker-row input[type="color"]::-webkit-color-swatch { + border: none; + border-radius: 50%; + } + .color-label { + flex: 1; + font-size: 14px; + } + .color-reset-btn { + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + cursor: pointer; + } + .color-reset-btn:hover { + color: var(--primary-color); + } `; } @@ -193,14 +308,7 @@ class PowerFluxCardEditor extends LitElement {

${this._localize('editor.solar_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.solar, 'solar', this._localize('editor.entity'))}
@@ -222,6 +330,10 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_solar', this._localize('editor.color_picker'), '#ffdd00')} +
@@ -253,23 +365,16 @@ class PowerFluxCardEditor extends LitElement {

${this._localize('editor.grid_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))} +
+ ${this._localize('editor.grid_combined_hint')} +
- +
+ + ${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")} + + ${this._renderEntitySelector(entitySelectorSchema, entities.grid_export, 'grid_export', this._localize('card.label_export') + " (W, Optional)")}
@@ -291,6 +396,10 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_grid', this._localize('editor.color_picker'), '#3b82f6')} +
@@ -322,23 +431,14 @@ class PowerFluxCardEditor extends LitElement {

${this._localize('editor.battery_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} - + ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} + + ${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))} +
+ ${this._localize('editor.grid_to_battery_hint')} +
@@ -359,6 +459,10 @@ class PowerFluxCardEditor extends LitElement { .label=${this._localize('editor.icon') + " (Optional)"} @value-changed=${this._valueChanged} > + + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_battery || "", 'secondary_battery', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_battery', this._localize('editor.color_picker'), '#00ff88')}
@@ -402,14 +506,7 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.house_total_title')}
- + ${this._renderEntitySelector(entitySelectorSchema, entities.house || "", 'house', this._localize('editor.house_sensor_label'))}
${this._localize('editor.house_sensor_hint')}
@@ -417,14 +514,7 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.consumer_1_title')}
- + ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_1, 'consumer_1', this._localize('editor.entity'))} + +
+ ${this._localize('editor.invert_consumer_1')} + +
+ + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_1 || "", 'secondary_consumer_1', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_consumer_1', this._localize('editor.color_picker'), '#a855f7')}
${this._localize('editor.consumer_2_title')}
- + ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_2, 'consumer_2', this._localize('editor.entity'))} + + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_consumer_2', this._localize('editor.color_picker'), '#f97316')}
${this._localize('editor.consumer_3_title')}
- + ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_3, 'consumer_3', this._localize('editor.entity'))} + + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_consumer_3', this._localize('editor.color_picker'), '#06b6d4')}
`; } @@ -600,6 +697,15 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.dashed_line')}
+
+ +
${this._localize('editor.tinted_background')}
+
+
`; } if (type === 'car') { - return html``; + const c = colorOverride || 'var(--consumer-1-color)'; + return html``; } if (type === 'heater') { - return html``; + const c = colorOverride || 'var(--consumer-2-color)'; + return html``; } if (type === 'pool') { - return html``; + const c = colorOverride || 'var(--consumer-3-color)'; + return html``; } return html``; } @@ -365,6 +420,11 @@ console.log( return Math.round(val) + " W"; } + _getConsumerColor(index) { + const style = getComputedStyle(this); + return style.getPropertyValue(`--consumer-${index}-color`).trim() || ['#a855f7', '#f97316', '#06b6d4'][index - 1]; + } + // --- DOM NODE SVG GENERATOR --- _renderSVGPath(d, color) { const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); @@ -420,19 +480,27 @@ console.log( }; const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0; - const gridMain = entities.grid ? getVal(entities.grid) : 0; + const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); + const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; + const gridMain = hasGridCombined ? gridCombinedVal : (entities.grid ? getVal(entities.grid) : 0); const gridExportSensor = entities.grid_export ? getVal(entities.grid_export) : 0; let battery = entities.battery ? getVal(entities.battery) : 0; if (this.config.invert_battery) { battery *= -1; } - const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value + let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value + if (this.config.invert_consumer_1) { c1Val *= -1; } + c1Val = Math.abs(c1Val); // 2. Logic Calculation let gridImport = 0; let gridExport = 0; - if (entities.grid_export && entities.grid_export !== "") { + if (hasGridCombined) { + // COMBINED SENSOR: positive = import, negative = export + gridImport = gridCombinedVal > 0 ? gridCombinedVal : 0; + gridExport = gridCombinedVal < 0 ? Math.abs(gridCombinedVal) : 0; + } else if (entities.grid_export && entities.grid_export !== "") { gridImport = gridMain > 0 ? gridMain : 0; gridExport = Math.abs(gridExportSensor); } else { @@ -447,12 +515,18 @@ console.log( let gridToBatt = 0; if (batteryCharge > 0) { - if (solar >= batteryCharge) { - solarToBatt = batteryCharge; - gridToBatt = 0; + const hasGridToBattSensor = !!(entities.grid_to_battery && entities.grid_to_battery !== ""); + if (hasGridToBattSensor) { + gridToBatt = Math.abs(getVal(entities.grid_to_battery)); + solarToBatt = Math.max(0, batteryCharge - gridToBatt); } else { - solarToBatt = solar; - gridToBatt = batteryCharge - solar; + if (solar >= batteryCharge) { + solarToBatt = batteryCharge; + gridToBatt = 0; + } else { + solarToBatt = solar; + gridToBatt = batteryCharge - solar; + } } } @@ -520,18 +594,18 @@ console.log( currentX += width; } - addSegment(srcBattery, 'var(--neon-yellow)', 'battery', 'battery', entities.battery); - addSegment(srcSolar, 'var(--neon-green)', 'solar', 'solar', entities.solar); - addSegment(srcGrid, 'var(--grid-grey)', 'grid', 'grid', entities.grid); + addSegment(srcBattery, 'var(--neon-green)', 'battery', 'battery', entities.battery); + addSegment(srcSolar, 'var(--neon-yellow)', 'solar', 'solar', entities.solar); + addSegment(srcGrid, 'var(--neon-blue)', 'grid', 'grid', entities.grid_combined || entities.grid); // --- GENERATE TOP BRACKETS (Based on Bar Segments) --- const topBrackets = barSegments.map(s => { const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); let icon = ''; let iconColor = ''; - if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-green)'; } - if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--grid-grey)'; } - if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-yellow)'; } + if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-yellow)'; } + if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--neon-blue)'; } + if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-green)'; } return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId }; }); @@ -550,7 +624,7 @@ console.log( let iconColor = ''; if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } - if (type === 'car') { icon = 'mdi:car-electric'; iconColor = '#a855f7'; } + if (type === 'car') { icon = 'mdi:car-electric'; iconColor = this._getConsumerColor(1); } if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; } @@ -569,7 +643,7 @@ console.log( addBottomBracket(destHouse, 'house', entities.house); addBottomBracket(destEV, 'car', entities.consumer_1); - addBottomBracket(destExport, 'export', entities.grid_export || entities.grid); + addBottomBracket(destExport, 'export', entities.grid_combined || entities.grid_export || entities.grid); addBottomBracket(batteryCharge, 'battery', entities.battery); // Note: If there is Battery Charging happening, bottomX will not reach fullWidth. @@ -651,7 +725,7 @@ console.log( // CUSTOM LABELS const labelSolarText = this.config.solar_label || this._localize('card.label_solar'); - const labelGridText = this.config.grid_label || this._localize('card.label_import'); + const labelGridText = this.config.grid_label || this._localize('card.label_grid'); const labelBatteryText = this.config.battery_label || (entities.battery && this.hass.states[entities.battery] && this.hass.states[entities.battery].state > 0 ? '+' : '-') + " " + this._localize('card.label_battery'); const labelHouseText = this.config.house_label || this._localize('card.label_house'); @@ -660,9 +734,31 @@ console.log( const iconGrid = this.config.grid_icon; const iconBattery = this.config.battery_icon; + // SECONDARY SENSORS (display only) + const hasSecondarySolar = !!(entities.secondary_solar && entities.secondary_solar !== ""); + const hasSecondaryGrid = !!(entities.secondary_grid && entities.secondary_grid !== ""); + const hasSecondaryBattery = !!(entities.secondary_battery && entities.secondary_battery !== ""); + + const getSecondaryVal = (entity) => { + if (!entity) return ''; + const state = this.hass.states[entity]; + if (!state) return ''; + const val = parseFloat(state.state); + if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : ''); + const unit = state.attributes.unit_of_measurement || ''; + if (unit === 'W' || unit === 'Wh') { + return this._formatPower(val); + } + if (unit === 'kWh' || unit === 'kW') { + return val.toFixed(1) + ' ' + unit; + } + return val.toFixed(1) + (unit ? ' ' + unit : ''); + }; + // Determine existence of main entities const hasSolar = !!(entities.solar && entities.solar !== ""); - const hasGrid = !!(entities.grid && entities.grid !== ""); + const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); + const hasGrid = !!(entities.grid && entities.grid !== "") || hasGridCombined; const hasBattery = !!(entities.battery && entities.battery !== ""); const styleSolar = hasSolar ? '' : 'display: none;'; @@ -683,7 +779,9 @@ console.log( return state ? parseFloat(state.state) || 0 : 0; }; - const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; + let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; + if (this.config.invert_consumer_1) { c1Val *= -1; } + c1Val = Math.abs(c1Val); const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0; const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 0; @@ -693,7 +791,8 @@ console.log( const anyBottomVisible = showC1 || showC2 || showC3; const solar = hasSolar ? getVal(entities.solar) : 0; - const gridMain = hasGrid ? getVal(entities.grid) : 0; + const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; + const gridMain = hasGridCombined ? gridCombinedVal : (hasGrid ? getVal(entities.grid) : 0); const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0; let battery = hasBattery ? getVal(entities.battery) : 0; if (this.config.invert_battery) { @@ -707,7 +806,11 @@ console.log( let gridExport = 0; if (hasGrid) { - if (entities.grid_export && entities.grid_export !== "") { + if (hasGridCombined) { + // COMBINED SENSOR: positive = import, negative = export + gridImport = gridCombinedVal > 0 ? gridCombinedVal : 0; + gridExport = gridCombinedVal < 0 ? Math.abs(gridCombinedVal) : 0; + } else if (entities.grid_export && entities.grid_export !== "") { gridImport = gridMain > 0 ? gridMain : 0; gridExport = Math.abs(gridExpSensor); } else { @@ -723,12 +826,20 @@ console.log( let gridToBatt = 0; if (hasBattery && batteryCharge > 0) { - if (solarVal >= batteryCharge) { - solarToBatt = batteryCharge; - gridToBatt = 0; + const hasGridToBattSensor = !!(entities.grid_to_battery && entities.grid_to_battery !== ""); + if (hasGridToBattSensor) { + // Use dedicated grid-to-battery sensor + gridToBatt = Math.abs(getVal(entities.grid_to_battery)); + solarToBatt = Math.max(0, batteryCharge - gridToBatt); } else { - solarToBatt = solarVal; - gridToBatt = batteryCharge - solarVal; + // Calculate: solar prioritized + if (solarVal >= batteryCharge) { + solarToBatt = batteryCharge; + gridToBatt = 0; + } else { + solarToBatt = solarVal; + gridToBatt = batteryCharge - solarVal; + } } } @@ -751,6 +862,8 @@ console.log( if (scale > 1.5) scale = 1.5; const finalCardHeightPx = contentHeight * scale; + const visualWidth = 420 * scale; + const centerMarginLeft = Math.max(0, (availableWidth - visualWidth) / 2); let houseGradientVal = ''; let houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; @@ -777,11 +890,11 @@ console.log( let stops = []; let current = 0; if (pctSolar > 0) { stops.push(`var(--neon-yellow) ${current}% ${current + pctSolar}%`); current += pctSolar; } - if (pctGrid > 0) { stops.push(`var(--neon-blue) ${current}% ${current + pctGrid}%`); current += pctGrid; } if (pctBatt > 0) { stops.push(`var(--neon-green) ${current}% ${current + pctBatt}%`); current += pctBatt; } + if (pctGrid > 0) { stops.push(`var(--neon-blue) ${current}% ${current + pctGrid}%`); current += pctGrid; } if (current < 99.9) { stops.push(`var(--neon-pink) ${current}% 100%`); } - houseGradientVal = `conic-gradient(${stops.join(', ')})`; + houseGradientVal = `conic-gradient(from 330deg, ${stops.join(', ')})`; if (useColoredValues) { const maxVal = Math.max(solarToHouse, gridToHouse, batteryDischarge); @@ -812,25 +925,51 @@ console.log( const houseBubbleStyle = `${showDonut ? `--house-gradient: ${houseGradientVal};` : ''} ${houseTintStyle} ${houseGlowStyle}`; const isSolarActive = Math.round(solarVal) > 0; - const isGridActive = Math.round(gridImport) > 0; + const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0; + const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0; const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; - const gridColor = isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'; + const gridColor = isGridExporting ? 'var(--neon-red)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); const getAnimStyle = (val) => { if (val <= 1) return "opacity: 0;"; - const userMinDuration = 7; - const userMaxDuration = 11; - const userFactor = 20000; - let duration = userFactor / val; - duration = Math.max(userMinDuration, Math.min(userMaxDuration, duration)); - // Adjust speed for dashed line (Factor to slow down: 5x) - if (showDashedLine) { - duration = duration * 5; + // --- Dynamic speed based on power --- + // Higher power = faster animation (shorter duration) + // Range: 2s (very fast, ~5000W+) to 12s (slow, ~50W) + const minDuration = 4; + const maxDuration = 12; + const factor = 12000; + let duration = factor / val; + duration = Math.max(minDuration, Math.min(maxDuration, duration)); + + // --- Dynamic particle density based on power --- + // Higher power = more/denser particles (shorter gap) + // Lower power = fewer/sparse particles (longer gap) + let dashSize, gapSize; + if (showTail) { + // Comet tail: vary tail length with power + dashSize = Math.round(15 + (val / 200) * 25); // 15-40 + dashSize = Math.min(dashSize, 40); + gapSize = Math.round(380 - (val / 200) * 200); // 380-180 + gapSize = Math.max(gapSize, 180); + } else if (showDashedLine) { + // Dashed line: vary dash density + dashSize = Math.round(8 + (val / 500) * 10); // 8-18 + dashSize = Math.min(dashSize, 18); + gapSize = Math.round(18 - (val / 1000) * 10); // 18-8 + gapSize = Math.max(gapSize, 8); + duration = duration * 5; // Dashed lines are slower + } else { + // Default dots: vary dot count/density + dashSize = 0; // stays as dots + gapSize = Math.round(380 - (val / 200) * 250); // 380-130 + gapSize = Math.max(gapSize, 130); } - return `opacity: 1; animation-duration: ${duration}s;`; + const dynamicDash = `${dashSize} ${gapSize}`; + + return `opacity: 1; animation-duration: ${duration}s; stroke-dasharray: ${dynamicDash};`; }; const getPipeStyle = (val) => { @@ -860,6 +999,14 @@ console.log( return html`
${text}
`; }; + const renderSecondaryOrLabel = (labelText, showLabel, secondaryEntity, hasSecondary) => { + if (hasSecondary) { + const secVal = getSecondaryVal(secondaryEntity); + return html`
${secVal}
`; + } + return renderLabel(labelText, showLabel); + }; + const renderMainIcon = (type, val, customIcon, color = null) => { if (customIcon) { const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--neon-yellow);' : (type === 'grid' ? 'color: var(--neon-blue);' : (type === 'battery' ? 'color: var(--neon-green);' : ''))); @@ -887,10 +1034,14 @@ console.log( iconContent = this._renderIcon(iconType, val); } + const secEntity = entities[`secondary_${configKey}`]; + const hasSecondary = !!(secEntity && secEntity !== ""); + return html` -
+
this._handleClick(entities[configKey])}> ${iconContent} - ${renderLabel(label, true)} + ${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)}
${this._formatPower(val)}
`; @@ -909,7 +1060,7 @@ console.log( const pathSolarHouse = "M 50 160 Q 50 265 165 265"; const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; const pathGridImport = "M 210 160 L 210 220"; - const pathGridExport = "M 165 115 Q 130 145 95 115"; + const pathGridExport = "M 95 115 Q 130 145 165 115"; const pathGridToBatt = "M 255 115 Q 290 145 325 115"; const pathBattHouse = "M 370 160 Q 370 265 255 265"; const pathHouseC1 = "M 165 265 Q 50 265 50 370"; @@ -923,7 +1074,7 @@ console.log( return html` -
+
@@ -937,9 +1088,9 @@ console.log( - - - + + + @@ -950,9 +1101,9 @@ console.log( - - - + + + ${this._formatPower(solarToHouse)} ${this._formatPower(solarToBatt)} @@ -966,36 +1117,43 @@ console.log( ${hasSolar ? html` -
+
this._handleClick(entities.solar)}> ${renderMainIcon('solar', solarVal, iconSolar, solarColor)} - ${renderLabel(labelSolarText, showLabelSolar)} + ${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)}
${this._formatPower(solarVal)}
` : ''} ${hasGrid ? html` -
- ${renderMainIcon('grid', gridImport, iconGrid, gridColor)} - ${renderLabel(labelGridText, showLabelGrid)} -
${this._formatPower(gridImport)}
+
this._handleClick(entities.grid_combined || entities.grid)}> + ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)} + ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} +
+ ${isGridExporting ? html`` : (isGridActive ? html`` : '')} + ${this._formatPower(isGridExporting ? gridExport : gridImport)} +
` : ''} ${hasBattery ? html` -
+
this._handleClick(entities.battery)}> ${renderMainIcon('battery', battSoc, iconBattery)} - ${renderLabel(labelBatteryText, showLabelBattery)} + ${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)}
${Math.round(battSoc)}%
` : ''}
+ style="${houseBubbleStyle}" + @click=${() => this._handleClick(entities.house)}> ${renderMainIcon('house', 0, null, houseDominantColor)} ${renderLabel(labelHouseText, showLabelHouse)}
${this._formatPower(house)}
- ${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, '#a855f7')} - ${renderConsumer(showC2, 'c2', 'consumer_2', labelC2, 'heater', c2Val, '#f97316')} - ${renderConsumer(showC3, 'c3', 'consumer_3', labelC3, 'pool', c3Val, '#06b6d4')} + ${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))} + ${renderConsumer(showC2, 'c2', 'consumer_2', labelC2, 'heater', c2Val, this._getConsumerColor(2))} + ${renderConsumer(showC3, 'c3', 'consumer_3', labelC3, 'pool', c3Val, this._getConsumerColor(3))}
From dba01153ba8b831a78006441e61ec720caa41a47 Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:29:20 +0000 Subject: [PATCH 12/20] power-flux-card Auto-build --- dist/power-flux-card.js | 608 +++++++++++++++++++++++++++++----------- 1 file changed, 444 insertions(+), 164 deletions(-) diff --git a/dist/power-flux-card.js b/dist/power-flux-card.js index 1028d3c..d39601d 100644 --- a/dist/power-flux-card.js +++ b/dist/power-flux-card.js @@ -34,8 +34,16 @@ const lang_de = { "editor.donut_chart": "Donut Chart (Grid/Haus)", "editor.comet_tail": "Comet Tail Effect", "editor.dashed_line": "Dashed Line Effect", + "editor.tinted_background": "Farbiger Hintergrund in Kreisen", "editor.colored_values": "Farbige Textwerte", "editor.hide_consumer_icons": "Icons unten ausblenden", + "editor.invert_consumer_1": "Sensorwert invertieren (+/-)", + "editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)", + "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)", + "editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", + "editor.grid_combined_sensor": "Kombinierter Netz-Sensor (W, Optional)", + "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt die getrennten Import/Export Sensoren.", + "editor.color_picker": "Farbe anpassen", }, card: { "card.label_solar": "Solar", @@ -78,8 +86,16 @@ const lang_en = { "editor.donut_chart": "Donut Chart (Grid/House)", "editor.comet_tail": "Comet Tail Effect", "editor.dashed_line": "Dashed Line Effect", + "editor.tinted_background": "Tinted Background in Bubbles", "editor.colored_values": "Colored Text Values", "editor.hide_consumer_icons": "Hide Consumer Icons", + "editor.invert_consumer_1": "Invert Sensor Value (+/-)", + "editor.secondary_sensor": "Secondary Sensor (display only)", + "editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)", + "editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", + "editor.grid_combined_sensor": "Combined Grid Sensor (W, Optional)", + "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides separate import/export sensors.", + "editor.color_picker": "Custom Color", }, card: { "card.label_solar": "Solar", @@ -165,10 +181,12 @@ class PowerFluxCardEditor extends LitElement { if (key) { const entityKeys = [ - 'solar', 'grid', 'grid_export', - 'battery', 'battery_soc', + 'solar', 'grid', 'grid_export', 'grid_combined', + 'battery', 'battery_soc', 'grid_to_battery', 'house', - 'consumer_1', 'consumer_2', 'consumer_3' + 'consumer_1', 'consumer_2', 'consumer_3', + 'secondary_solar', 'secondary_grid', 'secondary_battery', + 'secondary_consumer_1', 'secondary_consumer_2', 'secondary_consumer_3' ]; let newConfig = { ...this._config }; @@ -201,6 +219,65 @@ class PowerFluxCardEditor extends LitElement { this._subView = null; } + _clearEntity(key) { + const newConfig = { ...this._config }; + const currentEntities = newConfig.entities || {}; + const newEntities = { ...currentEntities, [key]: "" }; + newConfig.entities = newEntities; + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } + + _colorChanged(key, ev) { + const newConfig = { ...this._config, [key]: ev.target.value }; + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } + + _resetColor(key) { + const newConfig = { ...this._config }; + delete newConfig[key]; + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } + + _renderEntitySelector(entitySelectorSchema, value, configValue, label) { + const val = value || ""; + return html` +
+ + ${val ? html` this._clearEntity(configValue)} + >` : ''} +
+ `; + } + + _renderColorPicker(key, label, defaultColor) { + const currentColor = this._config[key] || defaultColor; + const hasCustom = !!this._config[key]; + return html` +
+ this._colorChanged(key, e)}> + ${label} + ${hasCustom ? html` this._resetColor(key)}>` : ''} +
+ `; + } + static get styles() { return css` .card-config { @@ -279,6 +356,60 @@ class PowerFluxCardEditor extends LitElement { border-bottom: 1px solid var(--divider-color); margin: 10px 0; } + .entity-picker-wrapper { + position: relative; + display: flex; + align-items: center; + gap: 4px; + } + .entity-picker-wrapper ha-selector { + flex: 1; + } + .clear-entity-btn { + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + cursor: pointer; + flex-shrink: 0; + margin-top: -12px; + } + .clear-entity-btn:hover { + color: var(--error-color, #db4437); + } + .color-picker-row { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 0; + } + .color-picker-row input[type="color"] { + -webkit-appearance: none; + border: 2px solid var(--divider-color); + border-radius: 50%; + width: 36px; + height: 36px; + padding: 2px; + cursor: pointer; + background: transparent; + } + .color-picker-row input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; + } + .color-picker-row input[type="color"]::-webkit-color-swatch { + border: none; + border-radius: 50%; + } + .color-label { + flex: 1; + font-size: 14px; + } + .color-reset-btn { + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + cursor: pointer; + } + .color-reset-btn:hover { + color: var(--primary-color); + } `; } @@ -293,14 +424,7 @@ class PowerFluxCardEditor extends LitElement {

${this._localize('editor.solar_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.solar, 'solar', this._localize('editor.entity'))}
@@ -322,6 +446,10 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_solar', this._localize('editor.color_picker'), '#ffdd00')} +
@@ -353,23 +481,16 @@ class PowerFluxCardEditor extends LitElement {

${this._localize('editor.grid_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))} +
+ ${this._localize('editor.grid_combined_hint')} +
- +
+ + ${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")} + + ${this._renderEntitySelector(entitySelectorSchema, entities.grid_export, 'grid_export', this._localize('card.label_export') + " (W, Optional)")}
@@ -391,6 +512,10 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_grid', this._localize('editor.color_picker'), '#3b82f6')} +
@@ -422,23 +547,14 @@ class PowerFluxCardEditor extends LitElement {

${this._localize('editor.battery_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} - + ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} + + ${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))} +
+ ${this._localize('editor.grid_to_battery_hint')} +
@@ -459,6 +575,10 @@ class PowerFluxCardEditor extends LitElement { .label=${this._localize('editor.icon') + " (Optional)"} @value-changed=${this._valueChanged} > + + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_battery || "", 'secondary_battery', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_battery', this._localize('editor.color_picker'), '#00ff88')}
@@ -502,14 +622,7 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.house_total_title')}
- + ${this._renderEntitySelector(entitySelectorSchema, entities.house || "", 'house', this._localize('editor.house_sensor_label'))}
${this._localize('editor.house_sensor_hint')}
@@ -517,14 +630,7 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.consumer_1_title')}
- + ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_1, 'consumer_1', this._localize('editor.entity'))} + +
+ ${this._localize('editor.invert_consumer_1')} + +
+ + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_1 || "", 'secondary_consumer_1', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_consumer_1', this._localize('editor.color_picker'), '#a855f7')}
${this._localize('editor.consumer_2_title')}
- + ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_2, 'consumer_2', this._localize('editor.entity'))} + + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_consumer_2', this._localize('editor.color_picker'), '#f97316')}
${this._localize('editor.consumer_3_title')}
- + ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_3, 'consumer_3', this._localize('editor.entity'))} + + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))} + + ${this._renderColorPicker('color_consumer_3', this._localize('editor.color_picker'), '#06b6d4')}
`; } @@ -700,6 +813,15 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.dashed_line')}
+
+ +
${this._localize('editor.tinted_background')}
+
+
`; } if (type === 'car') { - return html``; + const c = colorOverride || 'var(--consumer-1-color)'; + return html``; } if (type === 'heater') { - return html``; + const c = colorOverride || 'var(--consumer-2-color)'; + return html``; } if (type === 'pool') { - return html``; + const c = colorOverride || 'var(--consumer-3-color)'; + return html``; } return html``; } @@ -1111,6 +1288,11 @@ console.log( return Math.round(val) + " W"; } + _getConsumerColor(index) { + const style = getComputedStyle(this); + return style.getPropertyValue(`--consumer-${index}-color`).trim() || ['#a855f7', '#f97316', '#06b6d4'][index - 1]; + } + // --- DOM NODE SVG GENERATOR --- _renderSVGPath(d, color) { const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); @@ -1166,19 +1348,27 @@ console.log( }; const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0; - const gridMain = entities.grid ? getVal(entities.grid) : 0; + const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); + const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; + const gridMain = hasGridCombined ? gridCombinedVal : (entities.grid ? getVal(entities.grid) : 0); const gridExportSensor = entities.grid_export ? getVal(entities.grid_export) : 0; let battery = entities.battery ? getVal(entities.battery) : 0; if (this.config.invert_battery) { battery *= -1; } - const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value + let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value + if (this.config.invert_consumer_1) { c1Val *= -1; } + c1Val = Math.abs(c1Val); // 2. Logic Calculation let gridImport = 0; let gridExport = 0; - if (entities.grid_export && entities.grid_export !== "") { + if (hasGridCombined) { + // COMBINED SENSOR: positive = import, negative = export + gridImport = gridCombinedVal > 0 ? gridCombinedVal : 0; + gridExport = gridCombinedVal < 0 ? Math.abs(gridCombinedVal) : 0; + } else if (entities.grid_export && entities.grid_export !== "") { gridImport = gridMain > 0 ? gridMain : 0; gridExport = Math.abs(gridExportSensor); } else { @@ -1193,12 +1383,18 @@ console.log( let gridToBatt = 0; if (batteryCharge > 0) { - if (solar >= batteryCharge) { - solarToBatt = batteryCharge; - gridToBatt = 0; + const hasGridToBattSensor = !!(entities.grid_to_battery && entities.grid_to_battery !== ""); + if (hasGridToBattSensor) { + gridToBatt = Math.abs(getVal(entities.grid_to_battery)); + solarToBatt = Math.max(0, batteryCharge - gridToBatt); } else { - solarToBatt = solar; - gridToBatt = batteryCharge - solar; + if (solar >= batteryCharge) { + solarToBatt = batteryCharge; + gridToBatt = 0; + } else { + solarToBatt = solar; + gridToBatt = batteryCharge - solar; + } } } @@ -1266,18 +1462,18 @@ console.log( currentX += width; } - addSegment(srcBattery, 'var(--neon-yellow)', 'battery', 'battery', entities.battery); - addSegment(srcSolar, 'var(--neon-green)', 'solar', 'solar', entities.solar); - addSegment(srcGrid, 'var(--grid-grey)', 'grid', 'grid', entities.grid); + addSegment(srcBattery, 'var(--neon-green)', 'battery', 'battery', entities.battery); + addSegment(srcSolar, 'var(--neon-yellow)', 'solar', 'solar', entities.solar); + addSegment(srcGrid, 'var(--neon-blue)', 'grid', 'grid', entities.grid_combined || entities.grid); // --- GENERATE TOP BRACKETS (Based on Bar Segments) --- const topBrackets = barSegments.map(s => { const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); let icon = ''; let iconColor = ''; - if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-green)'; } - if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--grid-grey)'; } - if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-yellow)'; } + if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-yellow)'; } + if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--neon-blue)'; } + if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-green)'; } return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId }; }); @@ -1296,7 +1492,7 @@ console.log( let iconColor = ''; if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } - if (type === 'car') { icon = 'mdi:car-electric'; iconColor = '#a855f7'; } + if (type === 'car') { icon = 'mdi:car-electric'; iconColor = this._getConsumerColor(1); } if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; } @@ -1315,7 +1511,7 @@ console.log( addBottomBracket(destHouse, 'house', entities.house); addBottomBracket(destEV, 'car', entities.consumer_1); - addBottomBracket(destExport, 'export', entities.grid_export || entities.grid); + addBottomBracket(destExport, 'export', entities.grid_combined || entities.grid_export || entities.grid); addBottomBracket(batteryCharge, 'battery', entities.battery); // Note: If there is Battery Charging happening, bottomX will not reach fullWidth. @@ -1397,7 +1593,7 @@ console.log( // CUSTOM LABELS const labelSolarText = this.config.solar_label || this._localize('card.label_solar'); - const labelGridText = this.config.grid_label || this._localize('card.label_import'); + const labelGridText = this.config.grid_label || this._localize('card.label_grid'); const labelBatteryText = this.config.battery_label || (entities.battery && this.hass.states[entities.battery] && this.hass.states[entities.battery].state > 0 ? '+' : '-') + " " + this._localize('card.label_battery'); const labelHouseText = this.config.house_label || this._localize('card.label_house'); @@ -1406,9 +1602,31 @@ console.log( const iconGrid = this.config.grid_icon; const iconBattery = this.config.battery_icon; + // SECONDARY SENSORS (display only) + const hasSecondarySolar = !!(entities.secondary_solar && entities.secondary_solar !== ""); + const hasSecondaryGrid = !!(entities.secondary_grid && entities.secondary_grid !== ""); + const hasSecondaryBattery = !!(entities.secondary_battery && entities.secondary_battery !== ""); + + const getSecondaryVal = (entity) => { + if (!entity) return ''; + const state = this.hass.states[entity]; + if (!state) return ''; + const val = parseFloat(state.state); + if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : ''); + const unit = state.attributes.unit_of_measurement || ''; + if (unit === 'W' || unit === 'Wh') { + return this._formatPower(val); + } + if (unit === 'kWh' || unit === 'kW') { + return val.toFixed(1) + ' ' + unit; + } + return val.toFixed(1) + (unit ? ' ' + unit : ''); + }; + // Determine existence of main entities const hasSolar = !!(entities.solar && entities.solar !== ""); - const hasGrid = !!(entities.grid && entities.grid !== ""); + const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); + const hasGrid = !!(entities.grid && entities.grid !== "") || hasGridCombined; const hasBattery = !!(entities.battery && entities.battery !== ""); const styleSolar = hasSolar ? '' : 'display: none;'; @@ -1429,7 +1647,9 @@ console.log( return state ? parseFloat(state.state) || 0 : 0; }; - const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; + let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; + if (this.config.invert_consumer_1) { c1Val *= -1; } + c1Val = Math.abs(c1Val); const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0; const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 0; @@ -1439,7 +1659,8 @@ console.log( const anyBottomVisible = showC1 || showC2 || showC3; const solar = hasSolar ? getVal(entities.solar) : 0; - const gridMain = hasGrid ? getVal(entities.grid) : 0; + const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; + const gridMain = hasGridCombined ? gridCombinedVal : (hasGrid ? getVal(entities.grid) : 0); const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0; let battery = hasBattery ? getVal(entities.battery) : 0; if (this.config.invert_battery) { @@ -1453,7 +1674,11 @@ console.log( let gridExport = 0; if (hasGrid) { - if (entities.grid_export && entities.grid_export !== "") { + if (hasGridCombined) { + // COMBINED SENSOR: positive = import, negative = export + gridImport = gridCombinedVal > 0 ? gridCombinedVal : 0; + gridExport = gridCombinedVal < 0 ? Math.abs(gridCombinedVal) : 0; + } else if (entities.grid_export && entities.grid_export !== "") { gridImport = gridMain > 0 ? gridMain : 0; gridExport = Math.abs(gridExpSensor); } else { @@ -1469,12 +1694,20 @@ console.log( let gridToBatt = 0; if (hasBattery && batteryCharge > 0) { - if (solarVal >= batteryCharge) { - solarToBatt = batteryCharge; - gridToBatt = 0; + const hasGridToBattSensor = !!(entities.grid_to_battery && entities.grid_to_battery !== ""); + if (hasGridToBattSensor) { + // Use dedicated grid-to-battery sensor + gridToBatt = Math.abs(getVal(entities.grid_to_battery)); + solarToBatt = Math.max(0, batteryCharge - gridToBatt); } else { - solarToBatt = solarVal; - gridToBatt = batteryCharge - solarVal; + // Calculate: solar prioritized + if (solarVal >= batteryCharge) { + solarToBatt = batteryCharge; + gridToBatt = 0; + } else { + solarToBatt = solarVal; + gridToBatt = batteryCharge - solarVal; + } } } @@ -1497,6 +1730,8 @@ console.log( if (scale > 1.5) scale = 1.5; const finalCardHeightPx = contentHeight * scale; + const visualWidth = 420 * scale; + const centerMarginLeft = Math.max(0, (availableWidth - visualWidth) / 2); let houseGradientVal = ''; let houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; @@ -1523,11 +1758,11 @@ console.log( let stops = []; let current = 0; if (pctSolar > 0) { stops.push(`var(--neon-yellow) ${current}% ${current + pctSolar}%`); current += pctSolar; } - if (pctGrid > 0) { stops.push(`var(--neon-blue) ${current}% ${current + pctGrid}%`); current += pctGrid; } if (pctBatt > 0) { stops.push(`var(--neon-green) ${current}% ${current + pctBatt}%`); current += pctBatt; } + if (pctGrid > 0) { stops.push(`var(--neon-blue) ${current}% ${current + pctGrid}%`); current += pctGrid; } if (current < 99.9) { stops.push(`var(--neon-pink) ${current}% 100%`); } - houseGradientVal = `conic-gradient(${stops.join(', ')})`; + houseGradientVal = `conic-gradient(from 330deg, ${stops.join(', ')})`; if (useColoredValues) { const maxVal = Math.max(solarToHouse, gridToHouse, batteryDischarge); @@ -1558,25 +1793,51 @@ console.log( const houseBubbleStyle = `${showDonut ? `--house-gradient: ${houseGradientVal};` : ''} ${houseTintStyle} ${houseGlowStyle}`; const isSolarActive = Math.round(solarVal) > 0; - const isGridActive = Math.round(gridImport) > 0; + const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0; + const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0; const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; - const gridColor = isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'; + const gridColor = isGridExporting ? 'var(--neon-red)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); const getAnimStyle = (val) => { if (val <= 1) return "opacity: 0;"; - const userMinDuration = 7; - const userMaxDuration = 11; - const userFactor = 20000; - let duration = userFactor / val; - duration = Math.max(userMinDuration, Math.min(userMaxDuration, duration)); - // Adjust speed for dashed line (Factor to slow down: 5x) - if (showDashedLine) { - duration = duration * 5; + // --- Dynamic speed based on power --- + // Higher power = faster animation (shorter duration) + // Range: 2s (very fast, ~5000W+) to 12s (slow, ~50W) + const minDuration = 4; + const maxDuration = 12; + const factor = 12000; + let duration = factor / val; + duration = Math.max(minDuration, Math.min(maxDuration, duration)); + + // --- Dynamic particle density based on power --- + // Higher power = more/denser particles (shorter gap) + // Lower power = fewer/sparse particles (longer gap) + let dashSize, gapSize; + if (showTail) { + // Comet tail: vary tail length with power + dashSize = Math.round(15 + (val / 200) * 25); // 15-40 + dashSize = Math.min(dashSize, 40); + gapSize = Math.round(380 - (val / 200) * 200); // 380-180 + gapSize = Math.max(gapSize, 180); + } else if (showDashedLine) { + // Dashed line: vary dash density + dashSize = Math.round(8 + (val / 500) * 10); // 8-18 + dashSize = Math.min(dashSize, 18); + gapSize = Math.round(18 - (val / 1000) * 10); // 18-8 + gapSize = Math.max(gapSize, 8); + duration = duration * 5; // Dashed lines are slower + } else { + // Default dots: vary dot count/density + dashSize = 0; // stays as dots + gapSize = Math.round(380 - (val / 200) * 250); // 380-130 + gapSize = Math.max(gapSize, 130); } - return `opacity: 1; animation-duration: ${duration}s;`; + const dynamicDash = `${dashSize} ${gapSize}`; + + return `opacity: 1; animation-duration: ${duration}s; stroke-dasharray: ${dynamicDash};`; }; const getPipeStyle = (val) => { @@ -1606,6 +1867,14 @@ console.log( return html`
${text}
`; }; + const renderSecondaryOrLabel = (labelText, showLabel, secondaryEntity, hasSecondary) => { + if (hasSecondary) { + const secVal = getSecondaryVal(secondaryEntity); + return html`
${secVal}
`; + } + return renderLabel(labelText, showLabel); + }; + const renderMainIcon = (type, val, customIcon, color = null) => { if (customIcon) { const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--neon-yellow);' : (type === 'grid' ? 'color: var(--neon-blue);' : (type === 'battery' ? 'color: var(--neon-green);' : ''))); @@ -1633,10 +1902,14 @@ console.log( iconContent = this._renderIcon(iconType, val); } + const secEntity = entities[`secondary_${configKey}`]; + const hasSecondary = !!(secEntity && secEntity !== ""); + return html` -
+
this._handleClick(entities[configKey])}> ${iconContent} - ${renderLabel(label, true)} + ${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)}
${this._formatPower(val)}
`; @@ -1655,7 +1928,7 @@ console.log( const pathSolarHouse = "M 50 160 Q 50 265 165 265"; const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; const pathGridImport = "M 210 160 L 210 220"; - const pathGridExport = "M 165 115 Q 130 145 95 115"; + const pathGridExport = "M 95 115 Q 130 145 165 115"; const pathGridToBatt = "M 255 115 Q 290 145 325 115"; const pathBattHouse = "M 370 160 Q 370 265 255 265"; const pathHouseC1 = "M 165 265 Q 50 265 50 370"; @@ -1669,7 +1942,7 @@ console.log( return html` -
+
@@ -1683,9 +1956,9 @@ console.log( - - - + + + @@ -1696,9 +1969,9 @@ console.log( - - - + + + ${this._formatPower(solarToHouse)} ${this._formatPower(solarToBatt)} @@ -1712,36 +1985,43 @@ console.log( ${hasSolar ? html` -
+
this._handleClick(entities.solar)}> ${renderMainIcon('solar', solarVal, iconSolar, solarColor)} - ${renderLabel(labelSolarText, showLabelSolar)} + ${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)}
${this._formatPower(solarVal)}
` : ''} ${hasGrid ? html` -
- ${renderMainIcon('grid', gridImport, iconGrid, gridColor)} - ${renderLabel(labelGridText, showLabelGrid)} -
${this._formatPower(gridImport)}
+
this._handleClick(entities.grid_combined || entities.grid)}> + ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)} + ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} +
+ ${isGridExporting ? html`` : (isGridActive ? html`` : '')} + ${this._formatPower(isGridExporting ? gridExport : gridImport)} +
` : ''} ${hasBattery ? html` -
+
this._handleClick(entities.battery)}> ${renderMainIcon('battery', battSoc, iconBattery)} - ${renderLabel(labelBatteryText, showLabelBattery)} + ${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)}
${Math.round(battSoc)}%
` : ''}
+ style="${houseBubbleStyle}" + @click=${() => this._handleClick(entities.house)}> ${renderMainIcon('house', 0, null, houseDominantColor)} ${renderLabel(labelHouseText, showLabelHouse)}
${this._formatPower(house)}
- ${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, '#a855f7')} - ${renderConsumer(showC2, 'c2', 'consumer_2', labelC2, 'heater', c2Val, '#f97316')} - ${renderConsumer(showC3, 'c3', 'consumer_3', labelC3, 'pool', c3Val, '#06b6d4')} + ${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))} + ${renderConsumer(showC2, 'c2', 'consumer_2', labelC2, 'heater', c2Val, this._getConsumerColor(2))} + ${renderConsumer(showC3, 'c3', 'consumer_3', labelC3, 'pool', c3Val, this._getConsumerColor(3))}
From 7ecf0d53b6da46b9ee383121ec755f375d4cd22e Mon Sep 17 00:00:00 2001 From: jayjojayson Date: Mon, 2 Mar 2026 00:31:47 +0100 Subject: [PATCH 13/20] v_2.3 --- src/lang-de.js | 10 ++- src/lang-en.js | 10 ++- src/power-flux-card-editor.js | 90 ++++++++++++++++++++-- src/power-flux-card.js | 138 +++++++++++++++++++++++++--------- 4 files changed, 200 insertions(+), 48 deletions(-) diff --git a/src/lang-de.js b/src/lang-de.js index f7220c7..4401713 100644 --- a/src/lang-de.js +++ b/src/lang-de.js @@ -20,7 +20,7 @@ export default { "editor.battery_soc_label": "Ladestand (%)", "editor.house_total_title": "🏠 Gesamtverbrauch (optional)", "editor.house_sensor_label": "Sensor für Hausverbrauch (optional)", - "editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (compact view).", + "editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (more-details). Ansonsten wird der Hausverbrauch berechnet.", "editor.consumer_1_title": "🚗 Links (Lila)", "editor.consumer_2_title": "♨️ Mitte (Orange)", "editor.consumer_3_title": "🏊 Rechts (Türkis)", @@ -37,8 +37,12 @@ export default { "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)", "editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", "editor.grid_combined_sensor": "Kombinierter Netz-Sensor (W, Optional)", - "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt die getrennten Import/Export Sensoren.", - "editor.color_picker": "Farbe anpassen", + "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt den kombinierten Import/Export Sensor.", + "editor.color_picker": "Kreis Farbe", + "editor.pipe_color": "Rohr Farbe", + "editor.export_color": "Export Farbe", + "editor.consumer_unit_kw": "Sensor meldet in kW", + "editor.show_consumer_always": "Verbraucher bei null Watt anzeigen", }, card: { "card.label_solar": "Solar", diff --git a/src/lang-en.js b/src/lang-en.js index f955cb9..fa9b379 100644 --- a/src/lang-en.js +++ b/src/lang-en.js @@ -20,7 +20,7 @@ export default { "editor.battery_soc_label": "State of Charge (%)", "editor.house_total_title": "🏠 Total Consumption (optional)", "editor.house_sensor_label": "Sensor for House Consumption (optional)", - "editor.house_sensor_hint": "Required to make the house icon clickable (compact view).", + "editor.house_sensor_hint": "Required to make the house icon clickable (more-details). Otherwise, the house consumption is calculated.", "editor.consumer_1_title": "🚗 Left (Purple)", "editor.consumer_2_title": "♨️ Center (Orange)", "editor.consumer_3_title": "🏊 Right (Cyan)", @@ -37,8 +37,12 @@ export default { "editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)", "editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", "editor.grid_combined_sensor": "Combined Grid Sensor (W, Optional)", - "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides separate import/export sensors.", - "editor.color_picker": "Custom Color", + "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides combined import/export sensor.", + "editor.color_picker": "Bubble Color", + "editor.pipe_color": "Pipe Color", + "editor.export_color": "Export Color", + "editor.consumer_unit_kw": "Sensor reports in kW", + "editor.show_consumer_always": "Show Consumers at zero watts", }, card: { "card.label_solar": "Solar", diff --git a/src/power-flux-card-editor.js b/src/power-flux-card-editor.js index bf1177a..15c68c7 100644 --- a/src/power-flux-card-editor.js +++ b/src/power-flux-card-editor.js @@ -162,6 +162,35 @@ class PowerFluxCardEditor extends LitElement { `; } + _renderColorPickerDual(bubbleKey, pipeKey, defaultColor) { + const bubbleColor = this._config[bubbleKey] || defaultColor; + const pipeColor = this._config[pipeKey] || defaultColor; + const hasBubbleCustom = !!this._config[bubbleKey]; + const hasPipeCustom = !!this._config[pipeKey]; + return html` +
+
+ this._colorChanged(bubbleKey, e)}> + ${this._localize('editor.color_picker')} + ${hasBubbleCustom ? html` this._resetColor(bubbleKey)}>` : ''} +
+
+ this._colorChanged(pipeKey, e)}> + ${this._localize('editor.pipe_color')} + ${hasPipeCustom ? html` this._resetColor(pipeKey)}>` : ''} +
+
+ `; + } + static get styles() { return css` .card-config { @@ -294,6 +323,13 @@ class PowerFluxCardEditor extends LitElement { .color-reset-btn:hover { color: var(--primary-color); } + .color-picker-dual { + display: flex; + gap: 8px; + } + .color-picker-dual .color-picker-row { + flex: 1; + } `; } @@ -332,7 +368,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_solar', this._localize('editor.color_picker'), '#ffdd00')} + ${this._renderColorPickerDual('color_solar', 'color_pipe_solar', '#ffdd00')}
@@ -366,11 +402,13 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))} + +
+
${this._localize('editor.grid_combined_hint')}
-
${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")} @@ -398,7 +436,9 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_grid', this._localize('editor.color_picker'), '#3b82f6')} + ${this._renderColorPickerDual('color_grid', 'color_pipe_grid', '#3b82f6')} + + ${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')}
@@ -462,7 +502,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_battery || "", 'secondary_battery', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_battery', this._localize('editor.color_picker'), '#00ff88')} + ${this._renderColorPickerDual('color_battery', 'color_pipe_battery', '#00ff88')}
@@ -543,9 +583,18 @@ class PowerFluxCardEditor extends LitElement { >
+
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_1 || "", 'secondary_consumer_1', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_1', this._localize('editor.color_picker'), '#a855f7')} + ${this._renderColorPickerDual('color_consumer_1', 'color_pipe_consumer_1', '#a855f7')}
@@ -570,9 +619,18 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_2', this._localize('editor.color_picker'), '#f97316')} + ${this._renderColorPickerDual('color_consumer_2', 'color_pipe_consumer_2', '#f97316')}
@@ -597,9 +655,18 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_3', this._localize('editor.color_picker'), '#06b6d4')} + ${this._renderColorPickerDual('color_consumer_3', 'color_pipe_consumer_3', '#06b6d4')}
`; } @@ -733,6 +800,15 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.hide_inactive')}
+
+ +
${this._localize('editor.show_consumer_always')}
+
+
{ + return getVal(entity) * (isKw ? 1000 : 1); + }; const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0; const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); @@ -488,7 +526,7 @@ console.log( if (this.config.invert_battery) { battery *= -1; } - let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value + let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; // EV Value if (this.config.invert_consumer_1) { c1Val *= -1; } c1Val = Math.abs(c1Val); @@ -778,16 +816,20 @@ console.log( const state = this.hass.states[entity]; return state ? parseFloat(state.state) || 0 : 0; }; + const getValKw = (entity, isKw) => { + return getVal(entity) * (isKw ? 1000 : 1); + }; - let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; + let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; if (this.config.invert_consumer_1) { c1Val *= -1; } c1Val = Math.abs(c1Val); - const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0; - const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 0; + const c2Val = entities.consumer_2 ? getValKw(entities.consumer_2, this.config.consumer_2_unit_kw === true) : 0; + const c3Val = entities.consumer_3 ? getValKw(entities.consumer_3, this.config.consumer_3_unit_kw === true) : 0; - const showC1 = (entities.consumer_1 && Math.round(c1Val) > 0); - const showC2 = (entities.consumer_2 && Math.round(c2Val) > 0); - const showC3 = (entities.consumer_3 && Math.round(c3Val) > 0); + const alwaysShowConsumer = this.config.show_consumer_always === true; + const showC1 = (entities.consumer_1 && (alwaysShowConsumer || Math.round(c1Val) > 0)); + const showC2 = (entities.consumer_2 && (alwaysShowConsumer || Math.round(c2Val) > 0)); + const showC3 = (entities.consumer_3 && (alwaysShowConsumer || Math.round(c3Val) > 0)); const anyBottomVisible = showC1 || showC2 || showC3; const solar = hasSolar ? getVal(entities.solar) : 0; @@ -928,8 +970,28 @@ console.log( const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0; const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0; + // --- Grid Donut Gradient --- + let gridGradientVal = ''; + if (showDonut && hasGrid && isGridActive) { + const gridTotal = gridToHouse + gridToBatt + gridExport; + if (gridTotal > 0) { + const gPctToHouse = (gridToHouse / gridTotal) * 100; + const gPctToBatt = (gridToBatt / gridTotal) * 100; + const gPctExport = (gridExport / gridTotal) * 100; + let gStops = []; + let gCurrent = 0; + if (gPctToHouse > 0) { gStops.push(`var(--neon-blue) ${gCurrent}% ${gCurrent + gPctToHouse}%`); gCurrent += gPctToHouse; } + if (gPctToBatt > 0) { gStops.push(`var(--neon-green) ${gCurrent}% ${gCurrent + gPctToBatt}%`); gCurrent += gPctToBatt; } + if (gPctExport > 0) { gStops.push(`var(--export-color) ${gCurrent}% ${gCurrent + gPctExport}%`); gCurrent += gPctExport; } + if (gCurrent < 99.9) { gStops.push(`var(--neon-blue) ${gCurrent}% 100%`); } + gridGradientVal = `conic-gradient(from 330deg, ${gStops.join(', ')})`; + } else { + gridGradientVal = isGridExporting ? 'var(--export-color)' : 'var(--neon-blue)'; + } + } + const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; - const gridColor = isGridExporting ? 'var(--neon-red)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); + const gridColor = isGridExporting ? 'var(--export-color)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); const getAnimStyle = (val) => { if (val <= 1) return "opacity: 0;"; @@ -1061,6 +1123,11 @@ console.log( const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; const pathGridImport = "M 210 160 L 210 220"; const pathGridExport = "M 95 115 Q 130 145 165 115"; + const pathHouseExport = "M 210 220 L 210 160"; + const exportFromSolar = solarVal > 1; + const activeExportPath = exportFromSolar ? pathGridExport : pathHouseExport; + const exportTextX = exportFromSolar ? '130' : '185'; + const exportTextY = exportFromSolar ? '145' : '195'; const pathGridToBatt = "M 255 115 Q 290 145 325 115"; const pathBattHouse = "M 370 160 Q 370 265 255 265"; const pathHouseC1 = "M 165 265 Q 50 265 50 370"; @@ -1083,33 +1150,33 @@ console.log( - + - - - + + + - + - - - + + + ${this._formatPower(solarToHouse)} ${this._formatPower(solarToBatt)} ${this._formatPower(gridToHouse)} - ${this._formatPower(gridExport)} + ${this._formatPower(gridExport)} ${this._formatPower(gridToBatt)} ${this._formatPower(batteryDischarge)} @@ -1125,7 +1192,8 @@ console.log(
` : ''} ${hasGrid ? html` -
this._handleClick(entities.grid_combined || entities.grid)}> ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)} ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} From 67f6021cd5267a246be4d1e22c902a395603fb4c Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:38:55 +0000 Subject: [PATCH 14/20] power-flux-card Auto-build --- dist/power-flux-card.js | 248 ++++++++++++++++++++++++++++++++-------- 1 file changed, 200 insertions(+), 48 deletions(-) diff --git a/dist/power-flux-card.js b/dist/power-flux-card.js index d39601d..e100d7d 100644 --- a/dist/power-flux-card.js +++ b/dist/power-flux-card.js @@ -25,7 +25,7 @@ const lang_de = { "editor.battery_soc_label": "Ladestand (%)", "editor.house_total_title": "🏠 Gesamtverbrauch (optional)", "editor.house_sensor_label": "Sensor für Hausverbrauch (optional)", - "editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (compact view).", + "editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (more-details). Ansonsten wird der Hausverbrauch berechnet.", "editor.consumer_1_title": "🚗 Links (Lila)", "editor.consumer_2_title": "♨️ Mitte (Orange)", "editor.consumer_3_title": "🏊 Rechts (Türkis)", @@ -42,8 +42,12 @@ const lang_de = { "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)", "editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", "editor.grid_combined_sensor": "Kombinierter Netz-Sensor (W, Optional)", - "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt die getrennten Import/Export Sensoren.", - "editor.color_picker": "Farbe anpassen", + "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt den kombinierten Import/Export Sensor.", + "editor.color_picker": "Kreis Farbe", + "editor.pipe_color": "Rohr Farbe", + "editor.export_color": "Export Farbe", + "editor.consumer_unit_kw": "Sensor meldet in kW", + "editor.show_consumer_always": "Verbraucher bei null Watt anzeigen", }, card: { "card.label_solar": "Solar", @@ -77,7 +81,7 @@ const lang_en = { "editor.battery_soc_label": "State of Charge (%)", "editor.house_total_title": "🏠 Total Consumption (optional)", "editor.house_sensor_label": "Sensor for House Consumption (optional)", - "editor.house_sensor_hint": "Required to make the house icon clickable (compact view).", + "editor.house_sensor_hint": "Required to make the house icon clickable (more-details). Otherwise, the house consumption is calculated.", "editor.consumer_1_title": "🚗 Left (Purple)", "editor.consumer_2_title": "♨️ Center (Orange)", "editor.consumer_3_title": "🏊 Right (Cyan)", @@ -94,8 +98,12 @@ const lang_en = { "editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)", "editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", "editor.grid_combined_sensor": "Combined Grid Sensor (W, Optional)", - "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides separate import/export sensors.", - "editor.color_picker": "Custom Color", + "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides combined import/export sensor.", + "editor.color_picker": "Bubble Color", + "editor.pipe_color": "Pipe Color", + "editor.export_color": "Export Color", + "editor.consumer_unit_kw": "Sensor reports in kW", + "editor.show_consumer_always": "Show Consumers at zero watts", }, card: { "card.label_solar": "Solar", @@ -278,6 +286,35 @@ class PowerFluxCardEditor extends LitElement { `; } + _renderColorPickerDual(bubbleKey, pipeKey, defaultColor) { + const bubbleColor = this._config[bubbleKey] || defaultColor; + const pipeColor = this._config[pipeKey] || defaultColor; + const hasBubbleCustom = !!this._config[bubbleKey]; + const hasPipeCustom = !!this._config[pipeKey]; + return html` +
+
+ this._colorChanged(bubbleKey, e)}> + ${this._localize('editor.color_picker')} + ${hasBubbleCustom ? html` this._resetColor(bubbleKey)}>` : ''} +
+
+ this._colorChanged(pipeKey, e)}> + ${this._localize('editor.pipe_color')} + ${hasPipeCustom ? html` this._resetColor(pipeKey)}>` : ''} +
+
+ `; + } + static get styles() { return css` .card-config { @@ -410,6 +447,13 @@ class PowerFluxCardEditor extends LitElement { .color-reset-btn:hover { color: var(--primary-color); } + .color-picker-dual { + display: flex; + gap: 8px; + } + .color-picker-dual .color-picker-row { + flex: 1; + } `; } @@ -448,7 +492,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_solar', this._localize('editor.color_picker'), '#ffdd00')} + ${this._renderColorPickerDual('color_solar', 'color_pipe_solar', '#ffdd00')}
@@ -482,11 +526,13 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))} + +
+
${this._localize('editor.grid_combined_hint')}
-
${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")} @@ -514,7 +560,9 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_grid', this._localize('editor.color_picker'), '#3b82f6')} + ${this._renderColorPickerDual('color_grid', 'color_pipe_grid', '#3b82f6')} + + ${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')}
@@ -578,7 +626,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_battery || "", 'secondary_battery', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_battery', this._localize('editor.color_picker'), '#00ff88')} + ${this._renderColorPickerDual('color_battery', 'color_pipe_battery', '#00ff88')}
@@ -659,9 +707,18 @@ class PowerFluxCardEditor extends LitElement { >
+
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_1 || "", 'secondary_consumer_1', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_1', this._localize('editor.color_picker'), '#a855f7')} + ${this._renderColorPickerDual('color_consumer_1', 'color_pipe_consumer_1', '#a855f7')}
@@ -686,9 +743,18 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_2', this._localize('editor.color_picker'), '#f97316')} + ${this._renderColorPickerDual('color_consumer_2', 'color_pipe_consumer_2', '#f97316')}
@@ -713,9 +779,18 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_3', this._localize('editor.color_picker'), '#06b6d4')} + ${this._renderColorPickerDual('color_consumer_3', 'color_pipe_consumer_3', '#06b6d4')}
`; } @@ -849,6 +924,15 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.hide_inactive')}
+
+ +
${this._localize('editor.show_consumer_always')}
+
+
{ + return getVal(entity) * (isKw ? 1000 : 1); + }; const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0; const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); @@ -1356,7 +1478,7 @@ console.log( if (this.config.invert_battery) { battery *= -1; } - let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value + let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; // EV Value if (this.config.invert_consumer_1) { c1Val *= -1; } c1Val = Math.abs(c1Val); @@ -1646,16 +1768,20 @@ console.log( const state = this.hass.states[entity]; return state ? parseFloat(state.state) || 0 : 0; }; + const getValKw = (entity, isKw) => { + return getVal(entity) * (isKw ? 1000 : 1); + }; - let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; + let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; if (this.config.invert_consumer_1) { c1Val *= -1; } c1Val = Math.abs(c1Val); - const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0; - const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 0; + const c2Val = entities.consumer_2 ? getValKw(entities.consumer_2, this.config.consumer_2_unit_kw === true) : 0; + const c3Val = entities.consumer_3 ? getValKw(entities.consumer_3, this.config.consumer_3_unit_kw === true) : 0; - const showC1 = (entities.consumer_1 && Math.round(c1Val) > 0); - const showC2 = (entities.consumer_2 && Math.round(c2Val) > 0); - const showC3 = (entities.consumer_3 && Math.round(c3Val) > 0); + const alwaysShowConsumer = this.config.show_consumer_always === true; + const showC1 = (entities.consumer_1 && (alwaysShowConsumer || Math.round(c1Val) > 0)); + const showC2 = (entities.consumer_2 && (alwaysShowConsumer || Math.round(c2Val) > 0)); + const showC3 = (entities.consumer_3 && (alwaysShowConsumer || Math.round(c3Val) > 0)); const anyBottomVisible = showC1 || showC2 || showC3; const solar = hasSolar ? getVal(entities.solar) : 0; @@ -1796,8 +1922,28 @@ console.log( const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0; const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0; + // --- Grid Donut Gradient --- + let gridGradientVal = ''; + if (showDonut && hasGrid && isGridActive) { + const gridTotal = gridToHouse + gridToBatt + gridExport; + if (gridTotal > 0) { + const gPctToHouse = (gridToHouse / gridTotal) * 100; + const gPctToBatt = (gridToBatt / gridTotal) * 100; + const gPctExport = (gridExport / gridTotal) * 100; + let gStops = []; + let gCurrent = 0; + if (gPctToHouse > 0) { gStops.push(`var(--neon-blue) ${gCurrent}% ${gCurrent + gPctToHouse}%`); gCurrent += gPctToHouse; } + if (gPctToBatt > 0) { gStops.push(`var(--neon-green) ${gCurrent}% ${gCurrent + gPctToBatt}%`); gCurrent += gPctToBatt; } + if (gPctExport > 0) { gStops.push(`var(--export-color) ${gCurrent}% ${gCurrent + gPctExport}%`); gCurrent += gPctExport; } + if (gCurrent < 99.9) { gStops.push(`var(--neon-blue) ${gCurrent}% 100%`); } + gridGradientVal = `conic-gradient(from 330deg, ${gStops.join(', ')})`; + } else { + gridGradientVal = isGridExporting ? 'var(--export-color)' : 'var(--neon-blue)'; + } + } + const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; - const gridColor = isGridExporting ? 'var(--neon-red)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); + const gridColor = isGridExporting ? 'var(--export-color)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); const getAnimStyle = (val) => { if (val <= 1) return "opacity: 0;"; @@ -1929,6 +2075,11 @@ console.log( const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; const pathGridImport = "M 210 160 L 210 220"; const pathGridExport = "M 95 115 Q 130 145 165 115"; + const pathHouseExport = "M 210 220 L 210 160"; + const exportFromSolar = solarVal > 1; + const activeExportPath = exportFromSolar ? pathGridExport : pathHouseExport; + const exportTextX = exportFromSolar ? '130' : '185'; + const exportTextY = exportFromSolar ? '145' : '195'; const pathGridToBatt = "M 255 115 Q 290 145 325 115"; const pathBattHouse = "M 370 160 Q 370 265 255 265"; const pathHouseC1 = "M 165 265 Q 50 265 50 370"; @@ -1951,33 +2102,33 @@ console.log( - + - - - + + + - + - - - + + + ${this._formatPower(solarToHouse)} ${this._formatPower(solarToBatt)} ${this._formatPower(gridToHouse)} - ${this._formatPower(gridExport)} + ${this._formatPower(gridExport)} ${this._formatPower(gridToBatt)} ${this._formatPower(batteryDischarge)} @@ -1993,7 +2144,8 @@ console.log(
` : ''} ${hasGrid ? html` -
this._handleClick(entities.grid_combined || entities.grid)}> ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)} ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} From 4ad407f030290ef267e281bae0b9e248fc6641d9 Mon Sep 17 00:00:00 2001 From: jayjojayson Date: Tue, 3 Mar 2026 23:30:52 +0100 Subject: [PATCH 15/20] v_2.4 --- README.md | 148 ++++++++++++++++++++++++++++++ docs/README-de.md | 148 ++++++++++++++++++++++++++++++ src/lang-de.js | 21 +++-- src/lang-en.js | 15 +++- src/power-flux-card-editor.js | 112 +++++++++++++++-------- src/power-flux-card.js | 165 ++++++++++++++++++++++------------ 6 files changed, 503 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 66a0584..22fb0cb 100644 --- a/README.md +++ b/README.md @@ -84,3 +84,151 @@ You can configure the card directly via the visual editor in Home Assistant. - **Donut Chart**: Show the energy mix as a ring around the house. - **Comet Tail / Dashed Line**: Change the flow animation style. - **Compact View**: Switch to the bar chart layout. +- **Color Options**: Define custom colors for each source and consumer. +- **Grid Import/Export**: Configure separate or combined entities. +- **Grid-to-Battery**: Optional direct sensor for Grid-to-Battery flow. +- **Separate Battery Sensors**: Optional separate sensors for battery charge and discharge. +- **Secondary Sensors**: Display alternative values in the main circles (e.g., daily yield, current charge power). + + +
+ Custom Colors with card_mod and Jinja2 Templates + +With the [card_mod](https://github.com/thomasloven/lovelace-card-mod) integration, you can dynamically override the CSS variables of the Power Flux Card using Jinja2 templates. This allows you to change colors based on sensor values — e.g., green solar icon during production, grey when idle. + +### Available CSS Variables + +| Variable | Description | +|---|---| +| `--neon-yellow` | Bubble color Solar | +| `--neon-blue` | Bubble color Grid | +| `--neon-green` | Bubble color Battery | +| `--neon-pink` | Bubble color House | +| `--pipe-solar-color` | Pipe color Solar | +| `--pipe-grid-color` | Pipe color Grid | +| `--pipe-battery-color` | Pipe color Battery | +| `--icon-solar-color` | Icon color Solar | +| `--icon-grid-color` | Icon color Grid | +| `--icon-battery-color` | Icon color Battery | +| `--icon-house-color` | Icon color House | +| `--icon-consumer-1-color` | Icon color Consumer 1 | +| `--text-solar-color` | Text color Solar | +| `--text-grid-color` | Text color Grid | +| `--text-battery-color` | Text color Battery | +| `--text-house-color` | Text color House | +| `--text-consumer-1-color` | Text color Consumer 1 | +| `--consumer-1-color` | Bubble color Consumer 1 | +| `--consumer-2-color` | Bubble color Consumer 2 | +| `--consumer-3-color` | Bubble color Consumer 3 | +| `--export-color` | Color for Export | + +### Example 1: Solar Icon — green during production, grey when idle + +```yaml +type: custom:power-flux-card +entities: + solar: sensor.solar_power + grid: sensor.grid_power + battery: sensor.battery_power + battery_soc: sensor.battery_soc +card_mod: + style: | + power-flux-card { + {% if states('sensor.solar_power') | float > 0 %} + --icon-solar-color: #00ff88; + {% else %} + --icon-solar-color: #9e9e9e; + {% endif %} + } +``` + +### Example 2: Grid text color — red on export, blue on import + +```yaml +type: custom:power-flux-card +entities: + solar: sensor.solar_power + grid_combined: sensor.grid_power_combined + battery: sensor.battery_power + battery_soc: sensor.battery_soc +card_mod: + style: | + power-flux-card { + {% if states('sensor.grid_power_combined') | float < 0 %} + --text-grid-color: #ff3333; + {% else %} + --text-grid-color: #3b82f6; + {% endif %} + } +``` + +### Example 3: Battery bubble — color based on State of Charge (SoC) + +```yaml +type: custom:power-flux-card +entities: + solar: sensor.solar_power + grid: sensor.grid_power + battery: sensor.battery_power + battery_soc: sensor.battery_soc +card_mod: + style: | + power-flux-card { + {% set soc = states('sensor.battery_soc') | float %} + {% if soc > 80 %} + --neon-green: #00ff88; + {% elif soc > 30 %} + --neon-green: #f59e0b; + {% else %} + --neon-green: #ff3333; + {% endif %} + } +``` + +### Example 4: Consumer 1 pipe — visible only at high power, otherwise transparent + +```yaml +type: custom:power-flux-card +entities: + solar: sensor.solar_power + grid: sensor.grid_power + battery: sensor.battery_power + battery_soc: sensor.battery_soc + consumer_1: sensor.wallbox_power +card_mod: + style: | + power-flux-card { + {% if states('sensor.wallbox_power') | float > 500 %} + --pipe-consumer-1-color: #a855f7; + --icon-consumer-1-color: #a855f7; + {% else %} + --pipe-consumer-1-color: rgba(168, 85, 247, 0.2); + --icon-consumer-1-color: #9e9e9e; + {% endif %} + } +``` + +### Example 5: Multiple colors at once — night mode (everything dimmed when Solar = 0) + +```yaml +type: custom:power-flux-card +entities: + solar: sensor.solar_power + grid: sensor.grid_power + battery: sensor.battery_power + battery_soc: sensor.battery_soc + consumer_1: sensor.wallbox_power +card_mod: + style: | + power-flux-card { + {% if states('sensor.solar_power') | float == 0 %} + --icon-solar-color: #555555; + --text-solar-color: #777777; + --neon-yellow: #666633; + --pipe-solar-color: #444422; + {% endif %} + } +``` + +> **Note:** card_mod must be installed separately via HACS. Templates are evaluated on every state update, so colors change in real time. +
diff --git a/docs/README-de.md b/docs/README-de.md index 3e90900..6523845 100644 --- a/docs/README-de.md +++ b/docs/README-de.md @@ -86,3 +86,151 @@ Du kannst die Karte direkt über den visuellen Editor in Home Assistant konfigur - **Donut Chart**: Zeigt den Energiemix als Ring um das Haus an. - **Kometenschweif / Gestrichelte Linie**: Ändern Sie den Stil der Flussanimation. - **Kompakte Ansicht**: Wechseln Sie zum Balkendiagramm-Layout. +- **Farboptionen**: Definieren Sie benutzerdefinierte Farben für jede Quelle und Verbraucher. +- **Netz-Import/Export**: Konfigurieren Sie separate oder kombinierte Entitäten. +- **Netz-zu-Batterie**: Optionaler direkter Sensor für den Netz-zu-Batterie-Fluss. +- **Batterie getrennte Sensoren**: Optional separate Sensoren für Batterie-Ladung und -Entladung. +- **Sekundäre Sensoren**: Zeigen Sie alternative Werte in den Hauptkreisen an (z.B. Tagesertrag, aktuelle Ladeleistung). + + +
+ Custom Farben mit card_mod und Jinja2 Templates + +Mit der [card_mod](https://github.com/thomasloven/lovelace-card-mod) Integration können die CSS-Variablen der Power Flux Card dynamisch per Jinja2-Templates überschrieben werden. So lassen sich Farben abhängig von Sensorwerten ändern — z.B. Solar-Icon grün bei Produktion, grau bei Stillstand. + +### Verfügbare CSS-Variablen + +| Variable | Beschreibung | +|---|---| +| `--neon-yellow` | Bubble-Farbe Solar | +| `--neon-blue` | Bubble-Farbe Grid | +| `--neon-green` | Bubble-Farbe Batterie | +| `--neon-pink` | Bubble-Farbe Haus | +| `--pipe-solar-color` | Pipe-Farbe Solar | +| `--pipe-grid-color` | Pipe-Farbe Grid | +| `--pipe-battery-color` | Pipe-Farbe Batterie | +| `--icon-solar-color` | Icon-Farbe Solar | +| `--icon-grid-color` | Icon-Farbe Grid | +| `--icon-battery-color` | Icon-Farbe Batterie | +| `--icon-house-color` | Icon-Farbe Haus | +| `--icon-consumer-1-color` | Icon-Farbe Consumer 1 | +| `--text-solar-color` | Text-Farbe Solar | +| `--text-grid-color` | Text-Farbe Grid | +| `--text-battery-color` | Text-Farbe Batterie | +| `--text-house-color` | Text-Farbe Haus | +| `--text-consumer-1-color` | Text-Farbe Consumer 1 | +| `--consumer-1-color` | Bubble-Farbe Consumer 1 | +| `--consumer-2-color` | Bubble-Farbe Consumer 2 | +| `--consumer-3-color` | Bubble-Farbe Consumer 3 | +| `--export-color` | Farbe für Export | + +### Beispiel 1: Solar-Icon — grün bei Produktion, grau bei Stillstand + +```yaml +type: custom:power-flux-card +entities: + solar: sensor.solar_power + grid: sensor.grid_power + battery: sensor.battery_power + battery_soc: sensor.battery_soc +card_mod: + style: | + power-flux-card { + {% if states('sensor.solar_power') | float > 0 %} + --icon-solar-color: #00ff88; + {% else %} + --icon-solar-color: #9e9e9e; + {% endif %} + } +``` + +### Beispiel 2: Grid-Textfarbe — rot bei Export, blau bei Import + +```yaml +type: custom:power-flux-card +entities: + solar: sensor.solar_power + grid_combined: sensor.grid_power_combined + battery: sensor.battery_power + battery_soc: sensor.battery_soc +card_mod: + style: | + power-flux-card { + {% if states('sensor.grid_power_combined') | float < 0 %} + --text-grid-color: #ff3333; + {% else %} + --text-grid-color: #3b82f6; + {% endif %} + } +``` + +### Beispiel 3: Batterie-Bubble — Farbe nach Ladestand (SoC) + +```yaml +type: custom:power-flux-card +entities: + solar: sensor.solar_power + grid: sensor.grid_power + battery: sensor.battery_power + battery_soc: sensor.battery_soc +card_mod: + style: | + power-flux-card { + {% set soc = states('sensor.battery_soc') | float %} + {% if soc > 80 %} + --neon-green: #00ff88; + {% elif soc > 30 %} + --neon-green: #f59e0b; + {% else %} + --neon-green: #ff3333; + {% endif %} + } +``` + +### Beispiel 4: Consumer-1-Pipe — sichtbar nur bei hoher Leistung, sonst transparent + +```yaml +type: custom:power-flux-card +entities: + solar: sensor.solar_power + grid: sensor.grid_power + battery: sensor.battery_power + battery_soc: sensor.battery_soc + consumer_1: sensor.wallbox_power +card_mod: + style: | + power-flux-card { + {% if states('sensor.wallbox_power') | float > 500 %} + --pipe-consumer-1-color: #a855f7; + --icon-consumer-1-color: #a855f7; + {% else %} + --pipe-consumer-1-color: rgba(168, 85, 247, 0.2); + --icon-consumer-1-color: #9e9e9e; + {% endif %} + } +``` + +### Beispiel 5: Mehrere Farben gleichzeitig — Nachtmodus (alles gedimmt wenn Solar = 0) + +```yaml +type: custom:power-flux-card +entities: + solar: sensor.solar_power + grid: sensor.grid_power + battery: sensor.battery_power + battery_soc: sensor.battery_soc + consumer_1: sensor.wallbox_power +card_mod: + style: | + power-flux-card { + {% if states('sensor.solar_power') | float == 0 %} + --icon-solar-color: #555555; + --text-solar-color: #777777; + --neon-yellow: #666633; + --pipe-solar-color: #444422; + {% endif %} + } +``` + +> **Hinweis:** card_mod muss separat über HACS installiert werden. Die Templates werden bei jedem State-Update ausgewertet, die Farben ändern sich also in Echtzeit. +
\ No newline at end of file diff --git a/src/lang-de.js b/src/lang-de.js index 4401713..8e7845b 100644 --- a/src/lang-de.js +++ b/src/lang-de.js @@ -13,7 +13,7 @@ export default { "editor.label_toggle": "Label im Kreis anzeigen", "editor.compact_view": "Kompakte Ansicht (evcc)", "editor.hide_inactive": "Inaktive Röhren ausblenden", - "editor.entity": "Entität (Watt)", + "editor.entity": "Kombinierter Batterie Sensor (W)", "editor.label": "Beschriftung", "editor.icon": "Icon", "editor.back": "Zurück", @@ -34,15 +34,22 @@ export default { "editor.hide_consumer_icons": "Icons unten ausblenden", "editor.invert_consumer_1": "Sensorwert invertieren (+/-)", "editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)", - "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)", + "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (W, Optional)", "editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", "editor.grid_combined_sensor": "Kombinierter Netz-Sensor (W, Optional)", "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt den kombinierten Import/Export Sensor.", - "editor.color_picker": "Kreis Farbe", - "editor.pipe_color": "Rohr Farbe", - "editor.export_color": "Export Farbe", + "editor.color_picker": "Bubble", + "editor.pipe_color": "Pipe", + "editor.export_color": "Export", "editor.consumer_unit_kw": "Sensor meldet in kW", "editor.show_consumer_always": "Verbraucher bei null Watt anzeigen", + "editor.battery_charge_sensor": "Batterie-Ladung Sensor (W, Optional)", + "editor.battery_discharge_sensor": "Batterie-Entladung Sensor (W, Optional)", + "editor.battery_separate_hint": "Optional: Separate Sensoren für Laden/Entladen. Überschreiben den Hauptsensor für die Berechnung.", + "editor.consumer_1_hide_pipe": "Pipe bei geringer Leistung ausblenden", + "editor.consumer_pipe_threshold": "Pipe-Schwellenwert (Watt)", + "editor.text_color": "Text", + "editor.icon_color": "Icon", }, card: { "card.label_solar": "Solar", @@ -50,7 +57,7 @@ export default { "card.label_battery": "Batterie", "card.label_house": "Verbrauch", "card.label_car": "E-Auto", - "card.label_import": "Import", - "card.label_export": "Export", + "card.label_heater": "Heizung", + "card.label_pool": "Pool", } }; diff --git a/src/lang-en.js b/src/lang-en.js index fa9b379..46ae1ca 100644 --- a/src/lang-en.js +++ b/src/lang-en.js @@ -13,7 +13,7 @@ export default { "editor.label_toggle": "Show Label in Bubble", "editor.compact_view": "Compact View (evcc)", "editor.hide_inactive": "Hide Inactive Pipes", - "editor.entity": "Entity (Watt)", + "editor.entity": "Combined Battery Sensor (W)", "editor.label": "Label", "editor.icon": "Icon", "editor.back": "Back", @@ -34,7 +34,7 @@ export default { "editor.hide_consumer_icons": "Hide Consumer Icons", "editor.invert_consumer_1": "Invert Sensor Value (+/-)", "editor.secondary_sensor": "Secondary Sensor (display only)", - "editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)", + "editor.grid_to_battery_sensor": "Grid to Battery Sensor (W, optional)", "editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", "editor.grid_combined_sensor": "Combined Grid Sensor (W, Optional)", "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides combined import/export sensor.", @@ -43,6 +43,13 @@ export default { "editor.export_color": "Export Color", "editor.consumer_unit_kw": "Sensor reports in kW", "editor.show_consumer_always": "Show Consumers at zero watts", + "editor.battery_charge_sensor": "Battery Charge Sensor (W, Optional)", + "editor.battery_discharge_sensor": "Battery Discharge Sensor (W, Optional)", + "editor.battery_separate_hint": "Optional: Separate sensors for charge/discharge. Override the main sensor for calculations.", + "editor.consumer_1_hide_pipe": "Hide pipe at low power", + "editor.consumer_pipe_threshold": "Pipe Threshold (Watts)", + "editor.text_color": "Text Color", + "editor.icon_color": "Icon Color", }, card: { "card.label_solar": "Solar", @@ -50,7 +57,7 @@ export default { "card.label_battery": "Battery", "card.label_house": "Consumption", "card.label_car": "Car", - "card.label_import": "Import", - "card.label_export": "Export", + "card.label_heater": "Heater", + "card.label_pool": "Pool", } }; diff --git a/src/power-flux-card-editor.js b/src/power-flux-card-editor.js index 15c68c7..7702902 100644 --- a/src/power-flux-card-editor.js +++ b/src/power-flux-card-editor.js @@ -48,7 +48,7 @@ class PowerFluxCardEditor extends LitElement { if (!this._config || !this.hass) return; const target = ev.target; - const key = target.configValue || this._currentConfigValue; + const key = target.configValue; let value; if (target.tagName === 'HA-SWITCH') { @@ -67,6 +67,7 @@ class PowerFluxCardEditor extends LitElement { const entityKeys = [ 'solar', 'grid', 'grid_export', 'grid_combined', 'battery', 'battery_soc', 'grid_to_battery', + 'battery_charge', 'battery_discharge', 'house', 'consumer_1', 'consumer_2', 'consumer_3', 'secondary_solar', 'secondary_grid', 'secondary_battery', @@ -162,31 +163,30 @@ class PowerFluxCardEditor extends LitElement { `; } - _renderColorPickerDual(bubbleKey, pipeKey, defaultColor) { - const bubbleColor = this._config[bubbleKey] || defaultColor; - const pipeColor = this._config[pipeKey] || defaultColor; - const hasBubbleCustom = !!this._config[bubbleKey]; - const hasPipeCustom = !!this._config[pipeKey]; + _renderColorPickerQuad(bubbleKey, pipeKey, textKey, iconKey, defaultColor) { + const items = [ + { key: bubbleKey, label: this._localize('editor.color_picker'), default: defaultColor }, + ]; + if (pipeKey) items.push({ key: pipeKey, label: this._localize('editor.pipe_color'), default: defaultColor }); + items.push({ key: textKey, label: this._localize('editor.text_color'), default: defaultColor }); + items.push({ key: iconKey, label: this._localize('editor.icon_color'), default: defaultColor }); return html` -
-
- this._colorChanged(bubbleKey, e)}> - ${this._localize('editor.color_picker')} - ${hasBubbleCustom ? html` this._resetColor(bubbleKey)}>` : ''} -
-
- this._colorChanged(pipeKey, e)}> - ${this._localize('editor.pipe_color')} - ${hasPipeCustom ? html` this._resetColor(pipeKey)}>` : ''} -
+
+ ${items.map(item => { + const color = this._config[item.key] || item.default; + const hasCustom = !!this._config[item.key]; + return html` +
+ this._colorChanged(item.key, e)}> + ${item.label} + ${hasCustom ? html` this._resetColor(item.key)}>` : ''} +
+ `; + })}
`; } @@ -298,8 +298,8 @@ class PowerFluxCardEditor extends LitElement { -webkit-appearance: none; border: 2px solid var(--divider-color); border-radius: 50%; - width: 36px; - height: 36px; + width: 30px; + height: 30px; padding: 2px; cursor: pointer; background: transparent; @@ -323,11 +323,11 @@ class PowerFluxCardEditor extends LitElement { .color-reset-btn:hover { color: var(--primary-color); } - .color-picker-dual { + .color-picker-quad { display: flex; gap: 8px; } - .color-picker-dual .color-picker-row { + .color-picker-quad .color-picker-row { flex: 1; } `; @@ -368,7 +368,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))} - ${this._renderColorPickerDual('color_solar', 'color_pipe_solar', '#ffdd00')} + ${this._renderColorPickerQuad('color_solar', 'color_pipe_solar', 'color_text_solar', 'color_icon_solar', '#ffdd00')}
@@ -436,7 +436,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))} - ${this._renderColorPickerDual('color_grid', 'color_pipe_grid', '#3b82f6')} + ${this._renderColorPickerQuad('color_grid', 'color_pipe_grid', 'color_text_grid', 'color_icon_grid', '#3b82f6')} ${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')} @@ -473,13 +473,14 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} - ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} +
- ${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))}
- ${this._localize('editor.grid_to_battery_hint')} + ${this._localize('editor.battery_separate_hint')}
- + ${this._renderEntitySelector(entitySelectorSchema, entities.battery_charge || "", 'battery_charge', this._localize('editor.battery_charge_sensor'))} + ${this._renderEntitySelector(entitySelectorSchema, entities.battery_discharge || "", 'battery_discharge', this._localize('editor.battery_discharge_sensor'))} +
+
+ + ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} + +
+ +
+ ${this._localize('editor.grid_to_battery_hint')} +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))} + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_battery || "", 'secondary_battery', this._localize('editor.secondary_sensor'))} - ${this._renderColorPickerDual('color_battery', 'color_pipe_battery', '#00ff88')} + ${this._renderColorPickerQuad('color_battery', 'color_pipe_battery', 'color_text_battery', 'color_icon_battery', '#00ff88')}
@@ -550,6 +562,8 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.house_sensor_hint')}
+ + ${this._renderColorPickerQuad('color_house', null, 'color_text_house', 'color_icon_house', '#ff0080')}
@@ -583,6 +597,26 @@ class PowerFluxCardEditor extends LitElement { >
+
+ ${this._localize('editor.consumer_1_hide_pipe')} + +
+ + ${this._config.consumer_1_hide_pipe === true ? html` + + ` : ''} +
${this._localize('editor.consumer_unit_kw')}
@@ -630,7 +664,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))} - ${this._renderColorPickerDual('color_consumer_2', 'color_pipe_consumer_2', '#f97316')} + ${this._renderColorPickerQuad('color_consumer_2', 'color_pipe_consumer_2', 'color_text_consumer_2', 'color_icon_consumer_2', '#f97316')}
@@ -666,7 +700,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))} - ${this._renderColorPickerDual('color_consumer_3', 'color_pipe_consumer_3', '#06b6d4')} + ${this._renderColorPickerQuad('color_consumer_3', 'color_pipe_consumer_3', 'color_text_consumer_3', 'color_icon_consumer_3', '#06b6d4')}
`; } diff --git a/src/power-flux-card.js b/src/power-flux-card.js index 14d5dcd..4db2e67 100644 --- a/src/power-flux-card.js +++ b/src/power-flux-card.js @@ -3,7 +3,7 @@ import lang_en from "./lang-en.js"; import lang_de from "./lang-de.js"; console.log( - "%c⚡ Power Flux Card v_2.1 ready", + "%c⚡ Power Flux Card v_2.4 ready", "background: #d19525ff; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" ); @@ -44,6 +44,8 @@ console.log( consumer_2_unit_kw: false, consumer_3_unit_kw: false, show_consumer_always: false, + consumer_1_hide_pipe: false, + consumer_1_pipe_threshold: 0, show_donut_border: false, show_neon_glow: true, show_comet_tail: false, @@ -66,6 +68,8 @@ console.log( grid_combined: "", battery: "", battery_soc: "", + battery_charge: "", + battery_discharge: "", house: "", consumer_1: "", consumer_2: "", @@ -128,6 +132,21 @@ console.log( 'color_pipe_consumer_1': '--pipe-consumer-1-color', 'color_pipe_consumer_2': '--pipe-consumer-2-color', 'color_pipe_consumer_3': '--pipe-consumer-3-color', + 'color_house': '--neon-pink', + 'color_icon_solar': '--icon-solar-color', + 'color_icon_grid': '--icon-grid-color', + 'color_icon_battery': '--icon-battery-color', + 'color_icon_house': '--icon-house-color', + 'color_icon_consumer_1': '--icon-consumer-1-color', + 'color_icon_consumer_2': '--icon-consumer-2-color', + 'color_icon_consumer_3': '--icon-consumer-3-color', + 'color_text_solar': '--text-solar-color', + 'color_text_grid': '--text-grid-color', + 'color_text_battery': '--text-battery-color', + 'color_text_house': '--text-house-color', + 'color_text_consumer_1': '--text-consumer-1-color', + 'color_text_consumer_2': '--text-consumer-2-color', + 'color_text_consumer_3': '--text-consumer-3-color', }; for (const [configKey, cssVar] of Object.entries(colorMap)) { if (this.config[configKey]) { @@ -155,7 +174,6 @@ console.log( --neon-green: #00ff88; --neon-pink: #ff0080; --neon-red: #ff3333; - --grid-grey: #9e9e9e; --export-purple: #a855f7; --export-color: #ff3333; --consumer-1-color: #a855f7; @@ -167,6 +185,20 @@ console.log( --pipe-consumer-1-color: var(--consumer-1-color); --pipe-consumer-2-color: var(--consumer-2-color); --pipe-consumer-3-color: var(--consumer-3-color); + --icon-solar-color: var(--neon-yellow); + --icon-grid-color: var(--neon-blue); + --icon-battery-color: var(--neon-green); + --icon-house-color: var(--neon-pink); + --icon-consumer-1-color: var(--consumer-1-color); + --icon-consumer-2-color: var(--consumer-2-color); + --icon-consumer-3-color: var(--consumer-3-color); + --text-solar-color: var(--neon-yellow); + --text-grid-color: var(--neon-blue); + --text-battery-color: var(--neon-green); + --text-house-color: var(--neon-pink); + --text-consumer-1-color: var(--consumer-1-color); + --text-consumer-2-color: var(--consumer-2-color); + --text-consumer-3-color: var(--consumer-3-color); --flow-dasharray: 0 380; } :host([data-theme-light]) { @@ -175,7 +207,6 @@ console.log( --neon-green: #059669; --neon-pink: #db2777; --neon-red: #dc2626; - --grid-grey: #6b7280; --export-purple: #7c3aed; --export-color: #dc2626; --consumer-1-color: #7c3aed; @@ -256,11 +287,6 @@ console.log( overflow: hidden; } - /* Source Colors */ - .src-solar { background: var(--neon-yellow); color: black; } - .src-grid { background: var(--neon-blue); color: black; } - .src-battery { background: var(--neon-green); color: black; } - /* --- STANDARD VIEW STYLES --- */ .scale-wrapper { width: 420px; @@ -394,7 +420,7 @@ console.log( @keyframes dash { to { stroke-dashoffset: -1500; } } .flow-text { - font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; filter: transition: opacity 0.3s ease; + font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; transition: opacity 0.3s ease; } .flow-text.no-shadow { filter: none; } .text-solar { fill: var(--pipe-solar-color); } @@ -408,35 +434,36 @@ console.log( _renderIcon(type, val = 0, colorOverride = null) { if (type === 'solar') { const animate = Math.round(val) > 0 ? 'spin-slow' : ''; - const color = colorOverride || 'var(--neon-yellow)'; + const color = colorOverride || 'var(--icon-solar-color)'; return html``; } if (type === 'grid') { const animate = Math.round(val) > 0 ? 'pulse' : ''; - const color = colorOverride || 'var(--neon-blue)'; + const color = colorOverride || 'var(--icon-grid-color)'; return html``; } if (type === 'battery') { const soc = Math.min(Math.max(val, 0), 100) / 100; const rectHeight = 14 * soc; const rectY = 18 - rectHeight; - const rectColor = soc > 0.2 ? 'var(--neon-green)' : 'var(--neon-red)'; - return html``; + const strokeColor = colorOverride || 'var(--icon-battery-color)'; + const rectColor = soc > 0.2 ? strokeColor : 'var(--neon-red)'; + return html``; } if (type === 'house') { - const strokeColor = colorOverride || 'var(--neon-pink)'; + const strokeColor = colorOverride || 'var(--icon-house-color)'; return html``; } if (type === 'car') { - const c = colorOverride || 'var(--consumer-1-color)'; + const c = colorOverride || 'var(--icon-consumer-1-color)'; return html``; } if (type === 'heater') { - const c = colorOverride || 'var(--consumer-2-color)'; + const c = colorOverride || 'var(--icon-consumer-2-color)'; return html``; } if (type === 'pool') { - const c = colorOverride || 'var(--consumer-3-color)'; + const c = colorOverride || 'var(--icon-consumer-3-color)'; return html``; } return html``; @@ -546,8 +573,12 @@ console.log( gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; } - const batteryCharge = battery > 0 ? battery : 0; - const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + // Check for separate battery charge/discharge sensors + const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); + const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== ""); + + const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0); + const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0); let solarToBatt = 0; let gridToBatt = 0; @@ -641,9 +672,9 @@ console.log( const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); let icon = ''; let iconColor = ''; - if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-yellow)'; } - if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--neon-blue)'; } - if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-green)'; } + if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--icon-solar-color)'; } + if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--icon-grid-color)'; } + if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--icon-battery-color)'; } return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId }; }); @@ -661,10 +692,10 @@ console.log( let icon = ''; let iconColor = ''; - if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } - if (type === 'car') { icon = 'mdi:car-electric'; iconColor = this._getConsumerColor(1); } - if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } - if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; } + if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--icon-house-color)'; } + if (type === 'car') { icon = 'mdi:car-electric'; iconColor = 'var(--icon-consumer-1-color)'; } + if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-color)'; } + if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--icon-battery-color)'; } const path = this._createBracketPath(bottomX, width, 'up'); bottomBrackets.push({ @@ -707,14 +738,19 @@ console.log(
- ${barSegments.map(s => html` + ${barSegments.map(s => { + const textColor = s.type === 'solar' && this.config.color_text_solar ? 'var(--text-solar-color)' + : s.type === 'grid' && this.config.color_text_grid ? 'var(--text-grid-color)' + : s.type === 'battery' && this.config.color_text_battery ? 'var(--text-battery-color)' + : (s.color === 'var(--export-purple)' ? 'white' : 'black'); + return html`
s.entityId && this._handleClick(s.entityId)}> ${s.widthPx > 35 ? this._formatPower(s.val) : ''}
- `)} + `})}
@@ -808,9 +844,9 @@ console.log( const textClass = showNeonGlow ? 'flow-text' : 'flow-text no-shadow'; // Custom Labels for Consumers - const labelC1 = this.config.consumer_1_label || "E-Auto"; - const labelC2 = this.config.consumer_2_label || "Heizung"; - const labelC3 = this.config.consumer_3_label || "Pool"; + const labelC1 = this.config.consumer_1_label || this._localize('card.label_car'); + const labelC2 = this.config.consumer_2_label || this._localize('card.label_heater'); + const labelC3 = this.config.consumer_3_label || this._localize('card.label_pool'); const getVal = (entity) => { const state = this.hass.states[entity]; @@ -832,6 +868,11 @@ console.log( const showC3 = (entities.consumer_3 && (alwaysShowConsumer || Math.round(c3Val) > 0)); const anyBottomVisible = showC1 || showC2 || showC3; + // Consumer 1 pipe threshold + const hideC1Pipe = this.config.consumer_1_hide_pipe === true; + const c1PipeThreshold = this.config.consumer_1_pipe_threshold || 0; + const c1PipeActive = showC1 && (!hideC1Pipe || c1Val >= c1PipeThreshold); + const solar = hasSolar ? getVal(entities.solar) : 0; const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; const gridMain = hasGridCombined ? gridCombinedVal : (hasGrid ? getVal(entities.grid) : 0); @@ -861,8 +902,12 @@ console.log( } } - const batteryCharge = battery > 0 ? battery : 0; - const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + // Check for separate battery charge/discharge sensors + const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); + const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== ""); + + const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0); + const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0); let solarToBatt = 0; let gridToBatt = 0; @@ -889,6 +934,9 @@ console.log( const gridToHouse = Math.max(0, gridImport - gridToBatt); const house = solarToHouse + gridToHouse + batteryDischarge; + // Use house entity for display if defined, otherwise use calculated value + const houseDisplay = (entities.house && entities.house !== "") ? getVal(entities.house) : house; + const isTopArcActive = (solarToBatt > 0); const topShift = (isTopArcActive || (!hideInactive && hasSolar && hasBattery)) ? 0 : 50; let baseHeight = anyBottomVisible ? 480 : 340; @@ -990,8 +1038,10 @@ console.log( } } - const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; + const solarColor = isSolarActive ? 'var(--icon-solar-color)' : 'var(--secondary-text-color)'; const gridColor = isGridExporting ? 'var(--export-color)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); + const gridIconColor = (isGridActive && this.config.color_icon_grid) ? 'var(--icon-grid-color)' : gridColor; + const gridTextColor = (isGridActive && this.config.color_text_grid) ? 'var(--text-grid-color)' : gridColor; const getAnimStyle = (val) => { if (val <= 1) return "opacity: 0;"; @@ -1071,27 +1121,24 @@ console.log( const renderMainIcon = (type, val, customIcon, color = null) => { if (customIcon) { - const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--neon-yellow);' : (type === 'grid' ? 'color: var(--neon-blue);' : (type === 'battery' ? 'color: var(--neon-green);' : ''))); + const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--icon-solar-color);' : (type === 'grid' ? 'color: var(--icon-grid-color);' : (type === 'battery' ? 'color: var(--icon-battery-color);' : (type === 'house' ? 'color: var(--icon-house-color);' : '')))); return html``; } return this._renderIcon(type, val, color); }; - const getCustomClass = (icon) => icon ? 'has-custom-icon' : ''; - const renderConsumer = (isVisible, cssClass, configKey, label, iconType, val, hexColor) => { if (!isVisible) return html``; const customIcon = this.config[`${configKey}_icon`]; let iconContent; - const isCustom = !hideConsumerIcons && !!customIcon; - const dynamicClass = isCustom ? 'has-custom-icon' : ''; + const iconColorVar = `var(--icon-${configKey.replace(/_/g, '-')}-color)`; if (hideConsumerIcons) { iconContent = html``; } else if (customIcon) { - iconContent = html``; + iconContent = html``; } else { iconContent = this._renderIcon(iconType, val); } @@ -1099,12 +1146,16 @@ console.log( const secEntity = entities[`secondary_${configKey}`]; const hasSecondary = !!(secEntity && secEntity !== ""); + const textStyle = this.config[`color_text_${configKey}`] + ? `color: var(--text-${configKey.replace(/_/g, '-')}-color);` + : getConsumerColorStyle(hexColor); + return html` -
this._handleClick(entities[configKey])}> ${iconContent} ${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)} -
${this._formatPower(val)}
+
${this._formatPower(val)}
`; }; @@ -1134,7 +1185,9 @@ console.log( const pathHouseC2 = "M 210 310 L 210 370"; const pathHouseC3 = "M 255 265 Q 370 265 370 370"; - const houseTextStyle = houseTextCol ? `color: ${houseTextCol};` : ''; + const houseTextStyle = this.config.color_text_house + ? 'color: var(--text-house-color);' + : (houseTextCol ? `color: ${houseTextCol};` : ''); const dashArrayVal = showTail ? '30 360' : (showDashedLine ? '13 13' : '0 380'); const strokeWidthVal = showDashedLine ? 4 : 8; @@ -1155,7 +1208,7 @@ console.log( - + @@ -1168,7 +1221,7 @@ console.log( - + @@ -1184,39 +1237,39 @@ console.log( ${hasSolar ? html` -
this._handleClick(entities.solar)}> ${renderMainIcon('solar', solarVal, iconSolar, solarColor)} ${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)} -
${this._formatPower(solarVal)}
+
${this._formatPower(solarVal)}
` : ''} ${hasGrid ? html` -
this._handleClick(entities.grid_combined || entities.grid)}> - ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)} + ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridIconColor)} ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} -
+
${isGridExporting ? html`` : (isGridActive ? html`` : '')} ${this._formatPower(isGridExporting ? gridExport : gridImport)}
` : ''} ${hasBattery ? html` -
this._handleClick(entities.battery)}> ${renderMainIcon('battery', battSoc, iconBattery)} ${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)} -
${Math.round(battSoc)}%
+
${Math.round(battSoc)}%
` : ''} -
this._handleClick(entities.house)}> - ${renderMainIcon('house', 0, null, houseDominantColor)} + ${renderMainIcon('house', 0, null, this.config.color_icon_house ? 'var(--icon-house-color)' : houseDominantColor)} ${renderLabel(labelHouseText, showLabelHouse)} -
${this._formatPower(house)}
+
${this._formatPower(houseDisplay)}
${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))} From 9430bc9bcf1494a2528120c7d5fafadaff0d6ca4 Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:31:03 +0000 Subject: [PATCH 16/20] power-flux-card Auto-build --- dist/power-flux-card.js | 313 ++++++++++++++++++++++++++-------------- 1 file changed, 207 insertions(+), 106 deletions(-) diff --git a/dist/power-flux-card.js b/dist/power-flux-card.js index e100d7d..d65ef37 100644 --- a/dist/power-flux-card.js +++ b/dist/power-flux-card.js @@ -18,7 +18,7 @@ const lang_de = { "editor.label_toggle": "Label im Kreis anzeigen", "editor.compact_view": "Kompakte Ansicht (evcc)", "editor.hide_inactive": "Inaktive Röhren ausblenden", - "editor.entity": "Entität (Watt)", + "editor.entity": "Kombinierter Batterie Sensor (W)", "editor.label": "Beschriftung", "editor.icon": "Icon", "editor.back": "Zurück", @@ -39,15 +39,22 @@ const lang_de = { "editor.hide_consumer_icons": "Icons unten ausblenden", "editor.invert_consumer_1": "Sensorwert invertieren (+/-)", "editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)", - "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)", + "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (W, Optional)", "editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", "editor.grid_combined_sensor": "Kombinierter Netz-Sensor (W, Optional)", "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt den kombinierten Import/Export Sensor.", - "editor.color_picker": "Kreis Farbe", - "editor.pipe_color": "Rohr Farbe", - "editor.export_color": "Export Farbe", + "editor.color_picker": "Bubble", + "editor.pipe_color": "Pipe", + "editor.export_color": "Export", "editor.consumer_unit_kw": "Sensor meldet in kW", "editor.show_consumer_always": "Verbraucher bei null Watt anzeigen", + "editor.battery_charge_sensor": "Batterie-Ladung Sensor (W, Optional)", + "editor.battery_discharge_sensor": "Batterie-Entladung Sensor (W, Optional)", + "editor.battery_separate_hint": "Optional: Separate Sensoren für Laden/Entladen. Überschreiben den Hauptsensor für die Berechnung.", + "editor.consumer_1_hide_pipe": "Pipe bei geringer Leistung ausblenden", + "editor.consumer_pipe_threshold": "Pipe-Schwellenwert (Watt)", + "editor.text_color": "Text", + "editor.icon_color": "Icon", }, card: { "card.label_solar": "Solar", @@ -55,8 +62,8 @@ const lang_de = { "card.label_battery": "Batterie", "card.label_house": "Verbrauch", "card.label_car": "E-Auto", - "card.label_import": "Import", - "card.label_export": "Export", + "card.label_heater": "Heizung", + "card.label_pool": "Pool", } }; const lang_en = { @@ -74,7 +81,7 @@ const lang_en = { "editor.label_toggle": "Show Label in Bubble", "editor.compact_view": "Compact View (evcc)", "editor.hide_inactive": "Hide Inactive Pipes", - "editor.entity": "Entity (Watt)", + "editor.entity": "Combined Battery Sensor (W)", "editor.label": "Label", "editor.icon": "Icon", "editor.back": "Back", @@ -95,7 +102,7 @@ const lang_en = { "editor.hide_consumer_icons": "Hide Consumer Icons", "editor.invert_consumer_1": "Invert Sensor Value (+/-)", "editor.secondary_sensor": "Secondary Sensor (display only)", - "editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)", + "editor.grid_to_battery_sensor": "Grid to Battery Sensor (W, optional)", "editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", "editor.grid_combined_sensor": "Combined Grid Sensor (W, Optional)", "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides combined import/export sensor.", @@ -104,6 +111,13 @@ const lang_en = { "editor.export_color": "Export Color", "editor.consumer_unit_kw": "Sensor reports in kW", "editor.show_consumer_always": "Show Consumers at zero watts", + "editor.battery_charge_sensor": "Battery Charge Sensor (W, Optional)", + "editor.battery_discharge_sensor": "Battery Discharge Sensor (W, Optional)", + "editor.battery_separate_hint": "Optional: Separate sensors for charge/discharge. Override the main sensor for calculations.", + "editor.consumer_1_hide_pipe": "Hide pipe at low power", + "editor.consumer_pipe_threshold": "Pipe Threshold (Watts)", + "editor.text_color": "Text Color", + "editor.icon_color": "Icon Color", }, card: { "card.label_solar": "Solar", @@ -111,8 +125,8 @@ const lang_en = { "card.label_battery": "Battery", "card.label_house": "Consumption", "card.label_car": "Car", - "card.label_import": "Import", - "card.label_export": "Export", + "card.label_heater": "Heater", + "card.label_pool": "Pool", } }; @@ -172,7 +186,7 @@ class PowerFluxCardEditor extends LitElement { if (!this._config || !this.hass) return; const target = ev.target; - const key = target.configValue || this._currentConfigValue; + const key = target.configValue; let value; if (target.tagName === 'HA-SWITCH') { @@ -191,6 +205,7 @@ class PowerFluxCardEditor extends LitElement { const entityKeys = [ 'solar', 'grid', 'grid_export', 'grid_combined', 'battery', 'battery_soc', 'grid_to_battery', + 'battery_charge', 'battery_discharge', 'house', 'consumer_1', 'consumer_2', 'consumer_3', 'secondary_solar', 'secondary_grid', 'secondary_battery', @@ -286,31 +301,30 @@ class PowerFluxCardEditor extends LitElement { `; } - _renderColorPickerDual(bubbleKey, pipeKey, defaultColor) { - const bubbleColor = this._config[bubbleKey] || defaultColor; - const pipeColor = this._config[pipeKey] || defaultColor; - const hasBubbleCustom = !!this._config[bubbleKey]; - const hasPipeCustom = !!this._config[pipeKey]; + _renderColorPickerQuad(bubbleKey, pipeKey, textKey, iconKey, defaultColor) { + const items = [ + { key: bubbleKey, label: this._localize('editor.color_picker'), default: defaultColor }, + ]; + if (pipeKey) items.push({ key: pipeKey, label: this._localize('editor.pipe_color'), default: defaultColor }); + items.push({ key: textKey, label: this._localize('editor.text_color'), default: defaultColor }); + items.push({ key: iconKey, label: this._localize('editor.icon_color'), default: defaultColor }); return html` -
-
- this._colorChanged(bubbleKey, e)}> - ${this._localize('editor.color_picker')} - ${hasBubbleCustom ? html` this._resetColor(bubbleKey)}>` : ''} -
-
- this._colorChanged(pipeKey, e)}> - ${this._localize('editor.pipe_color')} - ${hasPipeCustom ? html` this._resetColor(pipeKey)}>` : ''} -
+
+ ${items.map(item => { + const color = this._config[item.key] || item.default; + const hasCustom = !!this._config[item.key]; + return html` +
+ this._colorChanged(item.key, e)}> + ${item.label} + ${hasCustom ? html` this._resetColor(item.key)}>` : ''} +
+ `; + })}
`; } @@ -422,8 +436,8 @@ class PowerFluxCardEditor extends LitElement { -webkit-appearance: none; border: 2px solid var(--divider-color); border-radius: 50%; - width: 36px; - height: 36px; + width: 30px; + height: 30px; padding: 2px; cursor: pointer; background: transparent; @@ -447,11 +461,11 @@ class PowerFluxCardEditor extends LitElement { .color-reset-btn:hover { color: var(--primary-color); } - .color-picker-dual { + .color-picker-quad { display: flex; gap: 8px; } - .color-picker-dual .color-picker-row { + .color-picker-quad .color-picker-row { flex: 1; } `; @@ -492,7 +506,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))} - ${this._renderColorPickerDual('color_solar', 'color_pipe_solar', '#ffdd00')} + ${this._renderColorPickerQuad('color_solar', 'color_pipe_solar', 'color_text_solar', 'color_icon_solar', '#ffdd00')}
@@ -560,7 +574,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))} - ${this._renderColorPickerDual('color_grid', 'color_pipe_grid', '#3b82f6')} + ${this._renderColorPickerQuad('color_grid', 'color_pipe_grid', 'color_text_grid', 'color_icon_grid', '#3b82f6')} ${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')} @@ -597,13 +611,14 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} - ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} +
- ${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))}
- ${this._localize('editor.grid_to_battery_hint')} + ${this._localize('editor.battery_separate_hint')}
- + ${this._renderEntitySelector(entitySelectorSchema, entities.battery_charge || "", 'battery_charge', this._localize('editor.battery_charge_sensor'))} + ${this._renderEntitySelector(entitySelectorSchema, entities.battery_discharge || "", 'battery_discharge', this._localize('editor.battery_discharge_sensor'))} +
+
+ + ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} + +
+ +
+ ${this._localize('editor.grid_to_battery_hint')} +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))} + ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_battery || "", 'secondary_battery', this._localize('editor.secondary_sensor'))} - ${this._renderColorPickerDual('color_battery', 'color_pipe_battery', '#00ff88')} + ${this._renderColorPickerQuad('color_battery', 'color_pipe_battery', 'color_text_battery', 'color_icon_battery', '#00ff88')}
@@ -674,6 +700,8 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.house_sensor_hint')}
+ + ${this._renderColorPickerQuad('color_house', null, 'color_text_house', 'color_icon_house', '#ff0080')}
@@ -707,6 +735,26 @@ class PowerFluxCardEditor extends LitElement { >
+
+ ${this._localize('editor.consumer_1_hide_pipe')} + +
+ + ${this._config.consumer_1_hide_pipe === true ? html` + + ` : ''} +
${this._localize('editor.consumer_unit_kw')}
@@ -754,7 +802,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))} - ${this._renderColorPickerDual('color_consumer_2', 'color_pipe_consumer_2', '#f97316')} + ${this._renderColorPickerQuad('color_consumer_2', 'color_pipe_consumer_2', 'color_text_consumer_2', 'color_icon_consumer_2', '#f97316')}
@@ -790,7 +838,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))} - ${this._renderColorPickerDual('color_consumer_3', 'color_pipe_consumer_3', '#06b6d4')} + ${this._renderColorPickerQuad('color_consumer_3', 'color_pipe_consumer_3', 'color_text_consumer_3', 'color_icon_consumer_3', '#06b6d4')}
`; } @@ -955,7 +1003,7 @@ customElements.define("power-flux-card-editor", PowerFluxCardEditor); console.log( - "%c⚡ Power Flux Card v_2.1 ready", + "%c⚡ Power Flux Card v_2.4 ready", "background: #d19525ff; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" ); @@ -996,6 +1044,8 @@ console.log( consumer_2_unit_kw: false, consumer_3_unit_kw: false, show_consumer_always: false, + consumer_1_hide_pipe: false, + consumer_1_pipe_threshold: 0, show_donut_border: false, show_neon_glow: true, show_comet_tail: false, @@ -1018,6 +1068,8 @@ console.log( grid_combined: "", battery: "", battery_soc: "", + battery_charge: "", + battery_discharge: "", house: "", consumer_1: "", consumer_2: "", @@ -1080,6 +1132,21 @@ console.log( 'color_pipe_consumer_1': '--pipe-consumer-1-color', 'color_pipe_consumer_2': '--pipe-consumer-2-color', 'color_pipe_consumer_3': '--pipe-consumer-3-color', + 'color_house': '--neon-pink', + 'color_icon_solar': '--icon-solar-color', + 'color_icon_grid': '--icon-grid-color', + 'color_icon_battery': '--icon-battery-color', + 'color_icon_house': '--icon-house-color', + 'color_icon_consumer_1': '--icon-consumer-1-color', + 'color_icon_consumer_2': '--icon-consumer-2-color', + 'color_icon_consumer_3': '--icon-consumer-3-color', + 'color_text_solar': '--text-solar-color', + 'color_text_grid': '--text-grid-color', + 'color_text_battery': '--text-battery-color', + 'color_text_house': '--text-house-color', + 'color_text_consumer_1': '--text-consumer-1-color', + 'color_text_consumer_2': '--text-consumer-2-color', + 'color_text_consumer_3': '--text-consumer-3-color', }; for (const [configKey, cssVar] of Object.entries(colorMap)) { if (this.config[configKey]) { @@ -1107,7 +1174,6 @@ console.log( --neon-green: #00ff88; --neon-pink: #ff0080; --neon-red: #ff3333; - --grid-grey: #9e9e9e; --export-purple: #a855f7; --export-color: #ff3333; --consumer-1-color: #a855f7; @@ -1119,6 +1185,20 @@ console.log( --pipe-consumer-1-color: var(--consumer-1-color); --pipe-consumer-2-color: var(--consumer-2-color); --pipe-consumer-3-color: var(--consumer-3-color); + --icon-solar-color: var(--neon-yellow); + --icon-grid-color: var(--neon-blue); + --icon-battery-color: var(--neon-green); + --icon-house-color: var(--neon-pink); + --icon-consumer-1-color: var(--consumer-1-color); + --icon-consumer-2-color: var(--consumer-2-color); + --icon-consumer-3-color: var(--consumer-3-color); + --text-solar-color: var(--neon-yellow); + --text-grid-color: var(--neon-blue); + --text-battery-color: var(--neon-green); + --text-house-color: var(--neon-pink); + --text-consumer-1-color: var(--consumer-1-color); + --text-consumer-2-color: var(--consumer-2-color); + --text-consumer-3-color: var(--consumer-3-color); --flow-dasharray: 0 380; } :host([data-theme-light]) { @@ -1127,7 +1207,6 @@ console.log( --neon-green: #059669; --neon-pink: #db2777; --neon-red: #dc2626; - --grid-grey: #6b7280; --export-purple: #7c3aed; --export-color: #dc2626; --consumer-1-color: #7c3aed; @@ -1208,11 +1287,6 @@ console.log( overflow: hidden; } - /* Source Colors */ - .src-solar { background: var(--neon-yellow); color: black; } - .src-grid { background: var(--neon-blue); color: black; } - .src-battery { background: var(--neon-green); color: black; } - /* --- STANDARD VIEW STYLES --- */ .scale-wrapper { width: 420px; @@ -1346,7 +1420,7 @@ console.log( @keyframes dash { to { stroke-dashoffset: -1500; } } .flow-text { - font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; filter: transition: opacity 0.3s ease; + font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; transition: opacity 0.3s ease; } .flow-text.no-shadow { filter: none; } .text-solar { fill: var(--pipe-solar-color); } @@ -1360,35 +1434,36 @@ console.log( _renderIcon(type, val = 0, colorOverride = null) { if (type === 'solar') { const animate = Math.round(val) > 0 ? 'spin-slow' : ''; - const color = colorOverride || 'var(--neon-yellow)'; + const color = colorOverride || 'var(--icon-solar-color)'; return html``; } if (type === 'grid') { const animate = Math.round(val) > 0 ? 'pulse' : ''; - const color = colorOverride || 'var(--neon-blue)'; + const color = colorOverride || 'var(--icon-grid-color)'; return html``; } if (type === 'battery') { const soc = Math.min(Math.max(val, 0), 100) / 100; const rectHeight = 14 * soc; const rectY = 18 - rectHeight; - const rectColor = soc > 0.2 ? 'var(--neon-green)' : 'var(--neon-red)'; - return html``; + const strokeColor = colorOverride || 'var(--icon-battery-color)'; + const rectColor = soc > 0.2 ? strokeColor : 'var(--neon-red)'; + return html``; } if (type === 'house') { - const strokeColor = colorOverride || 'var(--neon-pink)'; + const strokeColor = colorOverride || 'var(--icon-house-color)'; return html``; } if (type === 'car') { - const c = colorOverride || 'var(--consumer-1-color)'; + const c = colorOverride || 'var(--icon-consumer-1-color)'; return html``; } if (type === 'heater') { - const c = colorOverride || 'var(--consumer-2-color)'; + const c = colorOverride || 'var(--icon-consumer-2-color)'; return html``; } if (type === 'pool') { - const c = colorOverride || 'var(--consumer-3-color)'; + const c = colorOverride || 'var(--icon-consumer-3-color)'; return html``; } return html``; @@ -1498,8 +1573,12 @@ console.log( gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; } - const batteryCharge = battery > 0 ? battery : 0; - const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + // Check for separate battery charge/discharge sensors + const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); + const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== ""); + + const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0); + const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0); let solarToBatt = 0; let gridToBatt = 0; @@ -1593,9 +1672,9 @@ console.log( const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); let icon = ''; let iconColor = ''; - if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-yellow)'; } - if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--neon-blue)'; } - if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-green)'; } + if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--icon-solar-color)'; } + if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--icon-grid-color)'; } + if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--icon-battery-color)'; } return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId }; }); @@ -1613,10 +1692,10 @@ console.log( let icon = ''; let iconColor = ''; - if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } - if (type === 'car') { icon = 'mdi:car-electric'; iconColor = this._getConsumerColor(1); } - if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } - if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; } + if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--icon-house-color)'; } + if (type === 'car') { icon = 'mdi:car-electric'; iconColor = 'var(--icon-consumer-1-color)'; } + if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-color)'; } + if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--icon-battery-color)'; } const path = this._createBracketPath(bottomX, width, 'up'); bottomBrackets.push({ @@ -1659,14 +1738,19 @@ console.log(
- ${barSegments.map(s => html` + ${barSegments.map(s => { + const textColor = s.type === 'solar' && this.config.color_text_solar ? 'var(--text-solar-color)' + : s.type === 'grid' && this.config.color_text_grid ? 'var(--text-grid-color)' + : s.type === 'battery' && this.config.color_text_battery ? 'var(--text-battery-color)' + : (s.color === 'var(--export-purple)' ? 'white' : 'black'); + return html`
s.entityId && this._handleClick(s.entityId)}> ${s.widthPx > 35 ? this._formatPower(s.val) : ''}
- `)} + `})}
@@ -1760,9 +1844,9 @@ console.log( const textClass = showNeonGlow ? 'flow-text' : 'flow-text no-shadow'; // Custom Labels for Consumers - const labelC1 = this.config.consumer_1_label || "E-Auto"; - const labelC2 = this.config.consumer_2_label || "Heizung"; - const labelC3 = this.config.consumer_3_label || "Pool"; + const labelC1 = this.config.consumer_1_label || this._localize('card.label_car'); + const labelC2 = this.config.consumer_2_label || this._localize('card.label_heater'); + const labelC3 = this.config.consumer_3_label || this._localize('card.label_pool'); const getVal = (entity) => { const state = this.hass.states[entity]; @@ -1784,6 +1868,11 @@ console.log( const showC3 = (entities.consumer_3 && (alwaysShowConsumer || Math.round(c3Val) > 0)); const anyBottomVisible = showC1 || showC2 || showC3; + // Consumer 1 pipe threshold + const hideC1Pipe = this.config.consumer_1_hide_pipe === true; + const c1PipeThreshold = this.config.consumer_1_pipe_threshold || 0; + const c1PipeActive = showC1 && (!hideC1Pipe || c1Val >= c1PipeThreshold); + const solar = hasSolar ? getVal(entities.solar) : 0; const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; const gridMain = hasGridCombined ? gridCombinedVal : (hasGrid ? getVal(entities.grid) : 0); @@ -1813,8 +1902,12 @@ console.log( } } - const batteryCharge = battery > 0 ? battery : 0; - const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + // Check for separate battery charge/discharge sensors + const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); + const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== ""); + + const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0); + const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0); let solarToBatt = 0; let gridToBatt = 0; @@ -1841,6 +1934,9 @@ console.log( const gridToHouse = Math.max(0, gridImport - gridToBatt); const house = solarToHouse + gridToHouse + batteryDischarge; + // Use house entity for display if defined, otherwise use calculated value + const houseDisplay = (entities.house && entities.house !== "") ? getVal(entities.house) : house; + const isTopArcActive = (solarToBatt > 0); const topShift = (isTopArcActive || (!hideInactive && hasSolar && hasBattery)) ? 0 : 50; let baseHeight = anyBottomVisible ? 480 : 340; @@ -1942,8 +2038,10 @@ console.log( } } - const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; + const solarColor = isSolarActive ? 'var(--icon-solar-color)' : 'var(--secondary-text-color)'; const gridColor = isGridExporting ? 'var(--export-color)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); + const gridIconColor = (isGridActive && this.config.color_icon_grid) ? 'var(--icon-grid-color)' : gridColor; + const gridTextColor = (isGridActive && this.config.color_text_grid) ? 'var(--text-grid-color)' : gridColor; const getAnimStyle = (val) => { if (val <= 1) return "opacity: 0;"; @@ -2023,27 +2121,24 @@ console.log( const renderMainIcon = (type, val, customIcon, color = null) => { if (customIcon) { - const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--neon-yellow);' : (type === 'grid' ? 'color: var(--neon-blue);' : (type === 'battery' ? 'color: var(--neon-green);' : ''))); + const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--icon-solar-color);' : (type === 'grid' ? 'color: var(--icon-grid-color);' : (type === 'battery' ? 'color: var(--icon-battery-color);' : (type === 'house' ? 'color: var(--icon-house-color);' : '')))); return html``; } return this._renderIcon(type, val, color); }; - const getCustomClass = (icon) => icon ? 'has-custom-icon' : ''; - const renderConsumer = (isVisible, cssClass, configKey, label, iconType, val, hexColor) => { if (!isVisible) return html``; const customIcon = this.config[`${configKey}_icon`]; let iconContent; - const isCustom = !hideConsumerIcons && !!customIcon; - const dynamicClass = isCustom ? 'has-custom-icon' : ''; + const iconColorVar = `var(--icon-${configKey.replace(/_/g, '-')}-color)`; if (hideConsumerIcons) { iconContent = html``; } else if (customIcon) { - iconContent = html``; + iconContent = html``; } else { iconContent = this._renderIcon(iconType, val); } @@ -2051,12 +2146,16 @@ console.log( const secEntity = entities[`secondary_${configKey}`]; const hasSecondary = !!(secEntity && secEntity !== ""); + const textStyle = this.config[`color_text_${configKey}`] + ? `color: var(--text-${configKey.replace(/_/g, '-')}-color);` + : getConsumerColorStyle(hexColor); + return html` -
this._handleClick(entities[configKey])}> ${iconContent} ${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)} -
${this._formatPower(val)}
+
${this._formatPower(val)}
`; }; @@ -2086,7 +2185,9 @@ console.log( const pathHouseC2 = "M 210 310 L 210 370"; const pathHouseC3 = "M 255 265 Q 370 265 370 370"; - const houseTextStyle = houseTextCol ? `color: ${houseTextCol};` : ''; + const houseTextStyle = this.config.color_text_house + ? 'color: var(--text-house-color);' + : (houseTextCol ? `color: ${houseTextCol};` : ''); const dashArrayVal = showTail ? '30 360' : (showDashedLine ? '13 13' : '0 380'); const strokeWidthVal = showDashedLine ? 4 : 8; @@ -2107,7 +2208,7 @@ console.log( - + @@ -2120,7 +2221,7 @@ console.log( - + @@ -2136,39 +2237,39 @@ console.log( ${hasSolar ? html` -
this._handleClick(entities.solar)}> ${renderMainIcon('solar', solarVal, iconSolar, solarColor)} ${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)} -
${this._formatPower(solarVal)}
+
${this._formatPower(solarVal)}
` : ''} ${hasGrid ? html` -
this._handleClick(entities.grid_combined || entities.grid)}> - ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)} + ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridIconColor)} ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} -
+
${isGridExporting ? html`` : (isGridActive ? html`` : '')} ${this._formatPower(isGridExporting ? gridExport : gridImport)}
` : ''} ${hasBattery ? html` -
this._handleClick(entities.battery)}> ${renderMainIcon('battery', battSoc, iconBattery)} ${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)} -
${Math.round(battSoc)}%
+
${Math.round(battSoc)}%
` : ''} -
this._handleClick(entities.house)}> - ${renderMainIcon('house', 0, null, houseDominantColor)} + ${renderMainIcon('house', 0, null, this.config.color_icon_house ? 'var(--icon-house-color)' : houseDominantColor)} ${renderLabel(labelHouseText, showLabelHouse)} -
${this._formatPower(house)}
+
${this._formatPower(houseDisplay)}
${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))} From 2dc24eca1b642c726e3df050a020e707ee664627 Mon Sep 17 00:00:00 2001 From: jayjojayson Date: Wed, 4 Mar 2026 01:05:14 +0100 Subject: [PATCH 17/20] v_2.4 --- README.md | 10 +++++----- docs/README-de.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 22fb0cb..3d15de5 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ entities: battery_soc: sensor.battery_soc card_mod: style: | - power-flux-card { + :host { {% if states('sensor.solar_power') | float > 0 %} --icon-solar-color: #00ff88; {% else %} @@ -153,7 +153,7 @@ entities: battery_soc: sensor.battery_soc card_mod: style: | - power-flux-card { + :host { {% if states('sensor.grid_power_combined') | float < 0 %} --text-grid-color: #ff3333; {% else %} @@ -173,7 +173,7 @@ entities: battery_soc: sensor.battery_soc card_mod: style: | - power-flux-card { + :host { {% set soc = states('sensor.battery_soc') | float %} {% if soc > 80 %} --neon-green: #00ff88; @@ -197,7 +197,7 @@ entities: consumer_1: sensor.wallbox_power card_mod: style: | - power-flux-card { + :host { {% if states('sensor.wallbox_power') | float > 500 %} --pipe-consumer-1-color: #a855f7; --icon-consumer-1-color: #a855f7; @@ -220,7 +220,7 @@ entities: consumer_1: sensor.wallbox_power card_mod: style: | - power-flux-card { + :host { {% if states('sensor.solar_power') | float == 0 %} --icon-solar-color: #555555; --text-solar-color: #777777; diff --git a/docs/README-de.md b/docs/README-de.md index 6523845..8f75370 100644 --- a/docs/README-de.md +++ b/docs/README-de.md @@ -135,7 +135,7 @@ entities: battery_soc: sensor.battery_soc card_mod: style: | - power-flux-card { + :host { {% if states('sensor.solar_power') | float > 0 %} --icon-solar-color: #00ff88; {% else %} @@ -155,7 +155,7 @@ entities: battery_soc: sensor.battery_soc card_mod: style: | - power-flux-card { + :host { {% if states('sensor.grid_power_combined') | float < 0 %} --text-grid-color: #ff3333; {% else %} @@ -175,7 +175,7 @@ entities: battery_soc: sensor.battery_soc card_mod: style: | - power-flux-card { + :host { {% set soc = states('sensor.battery_soc') | float %} {% if soc > 80 %} --neon-green: #00ff88; @@ -199,7 +199,7 @@ entities: consumer_1: sensor.wallbox_power card_mod: style: | - power-flux-card { + :host { {% if states('sensor.wallbox_power') | float > 500 %} --pipe-consumer-1-color: #a855f7; --icon-consumer-1-color: #a855f7; @@ -222,7 +222,7 @@ entities: consumer_1: sensor.wallbox_power card_mod: style: | - power-flux-card { + :host { {% if states('sensor.solar_power') | float == 0 %} --icon-solar-color: #555555; --text-solar-color: #777777; From 97fd58f671124a57d12fddf94d4ead2e1588aaef Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:24:16 +0100 Subject: [PATCH 18/20] v_2.4 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d15de5..1ca85ac 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Stars](https://img.shields.io/github/stars/jayjojayson/power-flux-card)](https://github.com/jayjojayson/power-flux-card/stargazers) -# Power Flux Card +# Power Flux Card The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assistant. It visualizes the power distribution between Solar, Grid, Battery, and Consumers with beautiful neon effects and diffrent animations. From 1031af5b3d1731955c3b2c9d21cf05d3cafd96e1 Mon Sep 17 00:00:00 2001 From: jayjojayson <2683358+jayjojayson@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:04:31 +0100 Subject: [PATCH 19/20] v_2.4 --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 24336fc..128e009 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,3 @@ --- +ko_fi: jayjojayson custom: ["https://www.paypal.me/quadFlyerFW"] From f19730ff1da1e8c8815e8df6d04bfd6361369831 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Mar 2026 10:27:27 +0100 Subject: [PATCH 20/20] Fix: Secondary sensor display - Wh/EUR precision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Separate Wh from W in getSecondaryVal: Wh values >= 1000 now display as kWh (e.g., 794 Wh -> 0.79 kWh) - EUR/ct/€ units now show 2 decimal places (0.28 EUR/kWh instead of 0.3) - W values continue using _formatPower() as before Co-Authored-By: Claude Opus 4.6 --- dist/power-flux-card.js | 8 +------- src/power-flux-card.js | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/dist/power-flux-card.js b/dist/power-flux-card.js index d65ef37..2400503 100644 --- a/dist/power-flux-card.js +++ b/dist/power-flux-card.js @@ -1820,13 +1820,7 @@ console.log( const val = parseFloat(state.state); if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : ''); const unit = state.attributes.unit_of_measurement || ''; - if (unit === 'W' || unit === 'Wh') { - return this._formatPower(val); - } - if (unit === 'kWh' || unit === 'kW') { - return val.toFixed(1) + ' ' + unit; - } - return val.toFixed(1) + (unit ? ' ' + unit : ''); + if (unit === 'W') { return this._formatPower(val); } if (unit === 'Wh') { if (Math.abs(val) >= 1000) return (val / 1000).toFixed(2) + ' kWh'; return Math.round(val) + ' Wh'; } if (unit === 'kWh' || unit === 'kW') { return val.toFixed(1) + ' ' + unit; } if (unit.includes('EUR') || unit.includes('ct') || unit.includes('€')) { return val.toFixed(2) + ' ' + unit; } return val.toFixed(1) + (unit ? ' ' + unit : ''); }; // Determine existence of main entities diff --git a/src/power-flux-card.js b/src/power-flux-card.js index 4db2e67..842366d 100644 --- a/src/power-flux-card.js +++ b/src/power-flux-card.js @@ -820,13 +820,7 @@ console.log( const val = parseFloat(state.state); if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : ''); const unit = state.attributes.unit_of_measurement || ''; - if (unit === 'W' || unit === 'Wh') { - return this._formatPower(val); - } - if (unit === 'kWh' || unit === 'kW') { - return val.toFixed(1) + ' ' + unit; - } - return val.toFixed(1) + (unit ? ' ' + unit : ''); + if (unit === 'W') { return this._formatPower(val); } if (unit === 'Wh') { if (Math.abs(val) >= 1000) return (val / 1000).toFixed(2) + ' kWh'; return Math.round(val) + ' Wh'; } if (unit === 'kWh' || unit === 'kW') { return val.toFixed(1) + ' ' + unit; } if (unit.includes('EUR') || unit.includes('ct') || unit.includes('€')) { return val.toFixed(2) + ' ' + unit; } return val.toFixed(1) + (unit ? ' ' + unit : ''); }; // Determine existence of main entities