From 10a03b645c09f2bb7abdb17e9950d2333b318a1c Mon Sep 17 00:00:00 2001 From: Aaska Black Wolf Date: Sun, 24 May 2026 20:04:22 +0200 Subject: [PATCH] initial working status (overview for each device) --- .gitignore | 2 + README.md | 34 +++++- config.json | 4 + public/VEDirect.db | Bin 0 -> 86016 bytes public/dbQuery.php | 72 ++++++++++++ public/deviceIds.json | 155 +++++++++++++++++++++++++ public/i18n/de.json | 22 ++++ public/i18n/en.json | 22 ++++ public/images/README.md | 10 ++ public/index.html | 29 +++++ public/main.css | 36 ++++++ public/main.js | 243 ++++++++++++++++++++++++++++++++++++++++ 12 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 config.json create mode 100644 public/VEDirect.db create mode 100644 public/dbQuery.php create mode 100644 public/deviceIds.json create mode 100644 public/i18n/de.json create mode 100644 public/i18n/en.json create mode 100644 public/images/README.md create mode 100644 public/index.html create mode 100644 public/main.css create mode 100644 public/main.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af81d9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +public/images/*.jpg +public/images/*.png diff --git a/README.md b/README.md index 4bdae05..090d75e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ # VE.Direct-webui -Webbased User Interface to visualize the data collected by VE.Direct-Text-Reader \ No newline at end of file +Webbased User Interface to visualize the data collected by VE.Direct-Text-Reader + +## Prerequisites +This program is meant to run on a linux based computer. Something like a Raspberry Pi or similar system is more than sufficient. + +### VE.Direct-Text-Reader (database) +This is the front end to the database containing the records of any device information collected by [VE.Direct-Text-Reader](https://git.schauaus.at/VE-Tools/VE.Direct-Text-Reader). +Therefore the PHP process needs read access to that database. + +### PHP 8.5 +This project was developed for PHP 8.5 older and newer versions may work but are not tested. + +### Webserver +You need a webserver running PHP. This may be something lightweight like caddy. The program does not offer a web server service on its own. + +## Installation +Option 1: move this directory to your webserver (it may be any sub-folder inside your webserver's directory structure) +Option 2: configure your webserver to serve the files from this directory + +## Configuration + +### Path to database +For the frontend to work you need to configure the path to the database where VE.Direct-Text-Reader records all the data. +To do so, open `config.json` and edit the entry `database`according to your setup. You may use absolute or relative file paths. If you are using relative paths, ensure the path is relative to the main directory, not the `public` directory. + +### Show product images +We didn't as Victron Energy for the permission to use any of their product fotos. Therefore, product images are not part of this software and product pictures are disabled in the UI by default. +You may take your own pictures to show in your installation. To do so, you need to take 2 steps: +1. save your pictures to the folder `images` and rename it to {PID}.jpg or {PID}.png. Where {PID} needs to be replaced with the product ID of your product. (you can find that in the user interface) +2. open the file `config.json` and change the setting `showProductImages` to `true` + +## Access the user interface +To access the user interface open it on your favourite web browser. diff --git a/config.json b/config.json new file mode 100644 index 0000000..a7e976a --- /dev/null +++ b/config.json @@ -0,0 +1,4 @@ +{ + "database": "../VE.Direct-Text-Reader/VEDirect.db", + "showProductImages": false +} diff --git a/public/VEDirect.db b/public/VEDirect.db new file mode 100644 index 0000000000000000000000000000000000000000..535d68f92d7ebc08b2276a47ac0d57c0e17fc7a3 GIT binary patch literal 86016 zcmeI5dyE^$eaHDc@^}wgmKFJ>=zNZ&B(@dqlFQw_q<+MbEW5HTS+dSPiMl%~axB}j z8~fs?khMV80$B@WEs(W9;#%Nc`NE-R=TAO%YIW`OqmRrl%*~q(vweNTy54Bb8Lfqe zwa_r;%am(N{mVeNt^R-YpVt4o{vY*!t$(lnPxUwJZ`5C}|3&?^`pfl~>c3b2DtlYv z8Y26Hvlhr&AZvlF1+o^%S|DqItOc?b$XXz4fvg3x7Dz-3T+(|V{X4hzE~AeZ_p0=9 z3we@0UPSJvk9BeneViqG=;I85L;KTYjy_J2S^8KzOCKg5qz~15=|g27eVAy`hw?mq zC~blL|1G)y%++_yF3c=WuT6cb_Vvlvt3RlgEAtaKmXDX7E`GZ3QvUDgTay1lR<@L{ z+VM*}2Z<2FT4*+C9^~2iTMo}J%s1z+o8No)-Sk<5KHImr7hW@*ef#KV`wf?V);8d& z*`QC27CtrTQy1R8-!$pdmW5B7@CFl~AKn8$+N3Yv1}`??y$9~Pc@O>Mt~=@TI}RQ^ z;=SQ6{MAo2ybt=tg9nd#U%UI%!>dpCzXx74nx+ju>nOg(a1Hv|IkIw55YdEG&*%_M z9Ykn)8KT1>S`KY^iddH*j#kB?Avzx7Y!H$5)lsV0)KL*S{Dvx8R8y+rj07!_0w#LhVMV%UtzA({=Ni5;~fK}@iU zd`ER!3~?fe*c`2jTI(WM#SIn>vjIbZPB&!9LHE~1u!i?H;Y4VNt%MG4|)Rn%HE!q!DzMb~6wu@FR5^r&%F)IyB1F4|4zZt@bu z(W*GSj%vZu&*J~L!{YxFrE|s47hcJKH}`h(!+(K*oR|k~ui$b4ZNb(--yCu-0Dgre z696xZnE*&SuuK4aO}3CwE!Q&vO*YG(xJm-Zbzb<6qyp0E=o~pQ7X$=L*N;7t42Zd$ z6FVhGXf1aHEGNvS`DfxjX5q4A}U(B z3!-9}A?Fd>HVdmRlOT?EBx+f?u)H{*`nOsv=D0M7n1pm=n2J22-ELt=-7Y~Kt%^e% zi*A!8s@~y8v;<$82j6hOLYISZvuWINmBjOt6YP zqTOUm|HVN>-k*+OEN<$e!Dn&W1}yy$|6iQ5bM>9GH_UXV&rW@=_RY!fRDV|8Ua=<@ z%TJa*Q~Z;{oB99EZ>Mh>e2`8HTIMn?dUnwF^-U4jsN@8foUt`+bVQw|gpcdo$>PI} z2WH&+d}ff3cYKNBGq}x=ri+tP$B^(PRX?7u#dhpEjUXTI^%ARJi@BiA`z3s-(9b}Z z*V*movjvxzDE-(P#pai$fkwS^y@W3n`q8slG-B>t7v$spU83~k=NH>#xz%fdkI#csnkfV{O!tbpr)@09>1RY87+SqFSi5RgwjCQ3mcAg&Q7c1ZwJrQo}R zfM7zC8ANaYd3o4sjf zW%}b&U#k7tF z1RP)R<2ebom)tt;u#F5C@WhQaE3xWvct!&ckae6OUvfPr&)1|I$}~}IOZd{DAHDb( z`PxCgR5(r3X8qHW@TEsT7XLqyt6x34Z|3y$uTTAc?OT)oGkIzC1CIDxkrO=$#uDYxO*gU zsZs7o5H1xi7suRT30!KFyW0=fNU+8AkGVq?3R6SbqCzC58f!bf)-vDub_pCXU=<>zJE?wJS`#9 z?OEeR_CSzK+xU(b*+(Q~>Co){Aepx19WTvJNyyTn*@uHz>oCnaQ5vlNhZ zgJjw%)C;N^T@m`7FQ)?=@@4#K{9QFdqFh|c#vfYSvoX3;U_cF;u|{>vUF&6 zJV=%f-?(%P$iywre0&Ci5JU%Rr*oslHx*PCI2hAuMI82kNAVh;#qNWptpb2zv;ku zEyB-A$aK5dc#%C5Bul2*4H&1VC1mN)?5QAGO3dz)60&q?_Jl~*GMi~JyC0K~r9-o| zAXz%h?&9MTvUF(nSddKH>5kV*{ZR>7IyC!*Aer`wfbr7oQ3+W(G6`a9vPiHF+FZ$RH{t4D6^P#0gwVW7n!CqfVJmfD*$kHL&b3w9{ILLDnGOAhP47pOuSJ3M(NwdpB67IEaZ>pp4+^@zWupS#9 zMw$!FWD4gym%K=@4x5OU(DW!b;P6@-g7w#YXvtJ9fQEI~M6`rD%RqDyG1{bq#A^;=koLvks|`AS|} z^ry>8UUL^X8f}Z+k(Kyr%ON1w88iV~Q2|+<3PnKpqc#S_@aY)A;&OxgAp>CYU8>PE zF>i5i_k&gjS}_X01pexiFgS$teihEEEWJCBUo3?hnGzCcs$$kW=^asC*md4xbmIT!dk&} zYQdUvKD=bA$Ky2(T-c8htSBeqCDXvdQqaFK1D#sD&WZKpe0XV44{t<%j9@i69xt(1 zKndzK=sl@mS;ty(KD-pD*YL(3R+1C(Qs6nwCf49sB3MU`&)GNi>!~uQX?V9Lixd=X zqUYo~r_;i|xkTn6JQnE^YB@Q&rt9rh?-|;-2za={JhsD7=yXuY5hXJC~!uJ(h$Ulq0bkZDGGYLhK7+_In3wIAY1WUt- zfV@Q=OTb79GLzaNSRT#?m<9#y25usD$W1UGiGaLK9ZLlxb)H0WH-ODg3%IoJGB&IRwka`2eYc{qu5T*+nBvGG@c_x+s>669IX< zI&46#E((9tM$3fNPjX|`I3M5$$Dr0iA2tU0EWXPed52)#I1!Ndtix7N3oxvU@_-gQ zNYf#{%5fH5BVboQ`C|~uioz`FD;)=be5y2pg3z9979GS?k-cbB$BTw-G|@0|-HX~R zVbdYL+HnY|HU?t>YE3d>V~{i3^yhkB)K@$X02RGxEWlxnL9@YXQ+9~2dK?0dP(dxg z2nrf*->S9zUXVHRiW2HUlV9EL&XGGwzmCrqdVNE0Df2v&XD zDJaR`Q#FE;Oh3p*!JAoS2$zvZ5wfkrh-F5}; zinO?+Rfc2JbSp4o4LBd8+9b==10CTPGacJDK8>QItHWr)FO}&?d*^yzb=d~T3c*rv zBBP>Pl`-Pq5N*awtEnP6eK9m1TW=vifq-hF{1FlxzYgvbaJ>7c8!z-pObc{m?pd>Nx7YK_K7h>n99 zxMhMx;zUI7t;VzSw;c8sgALArNmXRwNCXwN7Dt>rB@>RxM~`kBxBEK;OU3aC4ts%K zL>(6q*5^p`jn>i_vib{vwuSq09fIZJc)<2%WKkn88d?WUA-k}Vg0_pB#vOts<9vYe z6&!AYBTDSyw$Q(I2$qc#0ToTEY!pTY)C!No77H%HVYm(&n-0O!aX!EiDyU`M!j=ji z&|*iXI%J*2y3^4s7zwc9DY#bxI6?)rx@Z(Ydwu_ZOYYTN{qk9B=HT>0Q_t7F zIQeq*+m#sJK2rGA{EPH0WA{P21S^(Z&3Q3bF@Ih+UpfYQmXidlnq4FGST$1?o07)d8a>u%gkZJ=Oo+eEk&~ngvnG$}C|Wa{ zTNaq9W;wRV1Jl+EA{vg|pb7aW7yRfsI{#q3GrrGKOtq027n5ckb<_ltE3u(spM{PB zlq(>nNU7%1Vv9*3H7LyxEg?m^9`rf}&13*2)=ht07<(zNYOK3FPElu|9B7#Gsf@;-(U66Ci8^TV#F=@KlP=A%>^mTe{=NXg_= zl+w|gVZ+i<0wvvpW5W{br-_t`NyWI7TA5P=b9$16M5J^v++QVFNKG7;{I%~J56d=F z5VT}z8bvMXuC>NG;)dij1W(|w8FrQ23#+9Fspez7Z3}>8nG8ipOG`@hl`8A~8YAK0cRR)RO!h^J1csiv^j{1EM-6vMd5i|V&l5Ht6CE3~% zB}uF|!8SrRiiJVxfY-zG|66h|=jxZunllHc@1Od3?X#1ASpD0|zg3D8JIZ$HQ1PL{ z^Z75*w~W^ZwK7=Ci<>j)RHC-|>kO=nw|CFw1ZlYK9H3Q}vDtGcr|5En8|q<4T5_V7^_JW$O~G@#UjbnonX=(l9_nO9K`p8703x zYTKS;!+KvLB{;U|Co;#4l6P!wf7-;_Up`7DE%6M9*JF9h7I|E1CgnoXrovXY!_H=Q z3DyA3biG0gf1c}JQE!*ThbKbCS8_lfO6EM@S)ROBA3 zY4D&6$z~sZ+rwptZoA~Pgi=k*xRmsin$Ea{?~zgRTH5Ti=1KAZQ1Y8QuHtn!v9%oL z9+u56@jFRABEeJ>ll4;w|Gl$sijkMC=kTxxNYoVbRcP1Q`Ttj7|GzbJ=kxQD0E~T&xHZaoY_OMQtNU3JrVr!{+5Iigcz~?y}b~>}$!;)D( zN;NG9$8zXZLxTa6B7Jo(fWejaAsL zi%U7gY>tCz4i^8vHHiP8ue~_=TJ;|*Z&%(i(JUVthxfomgb?N5M*r1Hm4~g9{MJs} z!W{B)56epNi0sz43O#jP5nXnyYPpBSrF@8LQ_i>|w%LsfmwQ-ZN<>r)n`0ts1zPYs zWm78GQR%viCjggwSZT_KI8qh$c9S%GXSs(Jr$j`>13NKQ)Ito)Vqt=zfBKKv z^5q^@p7J4%zlvd@GS^WJ7w*Avjx6`E0+ooUcvL5*idw0iFck$vc5Ui%4=Yjm5J#%w z@YY3k8eqAH6{$o-#V9$Zio-+f&!||L%7-{o6}6_Dkg-Tl_0p-9GwqM2OvPPeXP~0zxE0fK z^yr>Ox;*syPChT^-IJo*o03rK3aLq{mF2)=YcehQ$k;RE-Ciw73G=E}yrD^{wLXSj zdc>b4GhDjuj$4wG63P*4sWmKdGif6&9hhsq^Z!52)pyJ;%q&i?O?|5N^~u+(Kd6=~ z^Ak6gkC&b*d{9V-8VTC4sF;Tr~tiv7Aq4S?|HbAT6!qg{GR9G)< zb~>=r!zxYOO-Ja^RNMfGiKsKnZVX5ddE%brau4e?i5e=}x|o1kfpXZP4}P_D=+%Z8 zf9!&}$A_pk_go0XkZ#JFtABY_-<)H;CJ|B5*2P5J%#kP}vdcP`dswr{he%aaSSQC7 z(dnliv2GKOs9Nnc?tH3ccf$4oxUop58!x+xwVQm1BOQxc@dxxA$&w*-)b#}Nynnh8 zE51h9wu;$NwGcz1NIt|eELM_YxrbGpL=_b;7aUhayN@^*R8cYYIIf5;Tp<8?xt$Wk zk&ZJ}$AQap0r8zdM8#7&W2<5tBdg`!4hdp9RJ6T&sP?W5B97Lh4y~iQu;M~T zy+VShrs5!@GhQ33c0ap%c@R<2qsBzk@~ANWWlO)1dc-?8!v24G>ID}6ADw=jKFvO| z7RXv4Yk{ldatabase, '/')) ? $config->database : dirname(__FILE__) . '/../' . $config->database; +try { + $db = new SQLite3($dbPath, SQLITE3_OPEN_READONLY); +} +catch (Exception $e) { + returnJson(['error'=>'Cannot conect to database! ' . $e->getMessage()], 400); +} +switch ($_REQUEST['q']) { + case 'deviceData': + getDeviceData(); + break; + case 'devices': + getDevices(); + break; + case 'metadata': + getMetaData(); + break; + default: + returnJson(['error'=>'No request provided!'], 400); +} + +function getDeviceData() { + if(!isset($_REQUEST['device'])) returnJson(['error'=>'No device provided'], 400); + $sql = "SELECT date, time, data FROM '" . $_REQUEST['device'] . "' ORDER BY date ASC, time ASC;"; + $data = queryDb($sql); + if(!$data) returnJson(['error'=>'No data found'], 400); + returnJson($data); +} + +function getDevices () { + $sql = "SELECT name FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' AND name <> 'meta' ORDER BY name ASC;"; + $data = queryDb($sql); + if(!$data) returnJson(['error'=>'No devices found'], 400); + returnJson($data); +} + +function getMetaData() { + $sql = "SELECT * FROM meta;"; + $data = queryDb($sql); + if(!$data) returnJson(['error'=>'No metadata found in the database!'], 400); + returnJson($data); +} + +function queryDb($sql) { + global $db; + $res = $db->query($sql); + if(!$res) return false; + $data = []; + while ( $resLine = $res->fetchArray(SQLITE3_NUM) ) + array_push($data, $resLine); + if(count($data) == 0) return false; + return $data; +} + +function returnJson($data, $status = 200) { + http_response_code($status); //HTTP status code + header('Content-Type: application/json; charset=utf-8'); //HTTP headers + echo json_encode($data, JSON_UNESCAPED_UNICODE); //send data + exit(); +} +?> diff --git a/public/deviceIds.json b/public/deviceIds.json new file mode 100644 index 0000000..e4a399b --- /dev/null +++ b/public/deviceIds.json @@ -0,0 +1,155 @@ +{ + "0x203":"BMV-700", + "0x204":"BMV-702", + "0x205":"BMV-700H", + "0x0300":"BlueSolar MPPT 70|15", + "0xA040":"BlueSolar MPPT 75|50", + "0xA041":"BlueSolar MPPT 150|35", + "0xA042":"BlueSolar MPPT 75|15", + "0xA043":"BlueSolar MPPT 100|15", + "0xA044":"BlueSolar MPPT 100|30", + "0xA045":"BlueSolar MPPT 100|50", + "0xA046":"BlueSolar MPPT 150|70", + "0xA047":"BlueSolar MPPT 150|100", + "0xA049":"BlueSolar MPPT 100|50 rev2", + "0xA04A":"BlueSolar MPPT 100|30 rev2", + "0xA04B":"BlueSolar MPPT 150|35 rev2", + "0xA04C":"BlueSolar MPPT 75|10", + "0xA04D":"BlueSolar MPPT 150|45", + "0xA04E":"BlueSolar MPPT 150|60", + "0xA04F":"BlueSolar MPPT 150|85", + "0xA050":"SmartSolar MPPT 250|100", + "0xA051":"SmartSolar MPPT 150|100", + "0xA052":"SmartSolar MPPT 150|85", + "0xA053":"SmartSolar MPPT 75|15", + "0xA054":"SmartSolar MPPT 75|10", + "0xA055":"SmartSolar MPPT 100|15", + "0xA056":"SmartSolar MPPT 100|30", + "0xA057":"SmartSolar MPPT 100|50", + "0xA058":"SmartSolar MPPT 150|35", + "0xA059":"SmartSolar MPPT 150|100 rev2", + "0xA05A":"SmartSolar MPPT 150|85 rev2", + "0xA05B":"SmartSolar MPPT 250|70", + "0xA05C":"SmartSolar MPPT 250|85", + "0xA05D":"SmartSolar MPPT 250|60", + "0xA05E":"SmartSolar MPPT 250|45", + "0xA05F":"SmartSolar MPPT 100|20", + "0xA060":"SmartSolar MPPT 100|20 48V", + "0xA061":"SmartSolar MPPT 150|45", + "0xA062":"SmartSolar MPPT 150|60", + "0xA063":"SmartSolar MPPT 150|70", + "0xA064":"SmartSolar MPPT 250|85 rev2", + "0xA065":"SmartSolar MPPT 250|100 rev2", + "0xA066":"BlueSolar MPPT 100|20", + "0xA067":"BlueSolar MPPT 100|20 48V", + "0xA068":"SmartSolar MPPT 250|60 rev2", + "0xA069":"SmartSolar MPPT 250|70 rev2", + "0xA06A":"SmartSolar MPPT 150|45 rev2", + "0xA06B":"SmartSolar MPPT 150|60 rev2", + "0xA06C":"SmartSolar MPPT 150|70 rev2", + "0xA06D":"SmartSolar MPPT 150|85 rev3", + "0xA06E":"SmartSolar MPPT 150|100 rev3", + "0xA06F":"BlueSolar MPPT 150|45 rev2", + "0xA070":"BlueSolar MPPT 150|60 rev2", + "0xA071":"BlueSolar MPPT 150|70 rev2", + "0xA072":"BlueSolar MPPT 150/45 rev3", + "0xA073":"SmartSolar MPPT 150/45 rev3", + "0xA074":"SmartSolar MPPT 75 /10 rev2", + "0xA075":"SmartSolar MPPT 75 /15 rev2", + "0xA076":"BlueSolar MPPT 1 00/30 rev3", + "0xA077":"BlueSolar MPPT 1 00/50 rev3", + "0xA078":"BlueSolar MPPT 1 50/35 rev3", + "0xA079":"BlueSolar MPPT 75 /10 rev2", + "0xA07A":"BlueSolar MPPT 75 /15 rev2", + "0xA07B":"BlueSolar MPPT 100 /15 rev2", + "0xA07C":"BlueSolar MPPT 75 /10 rev3", + "0xA07D":"BlueSolar MPPT 75 /15 rev3", + "0xA07E":"SmartSolar MPPT 1 00/30 12V", + "0xA07F":"All -In -1 SmartSolar MPPT 75/15 12V", + "0xA080":"SmartSolar MPPT 250|60 rev3", + "0xA081":"SmartSolar MPPT 250|70 rev3", + "0xA102":"SmartSolar MPPT VE.Can 150/70", + "0xA103":"SmartSolar MPPT VE.Can 150/45", + "0xA104":"SmartSolar MPPT VE.Can 150/60", + "0xA105":"SmartSolar MPPT VE.Can 150/85", + "0xA106":"SmartSolar MPPT VE.Can 150/100", + "0xA107":"SmartSolar MPPT VE.Can 250/45", + "0xA108":"SmartSolar MPPT VE.Can 250/60", + "0xA109":"SmartSolar MPPT VE.Can 250/70", + "0xA10A":"SmartSolar MPPT VE.Can 250/85", + "0xA10B":"SmartSolar MPPT VE.Can 250/100", + "0xA10C":"SmartSolar MPPT VE.Can 150/70 rev2", + "0xA10D":"SmartSolar MPPT VE.Can 150/85 rev2", + "0xA10E":"SmartSolar MPPT VE.Can 150/100 rev2", + "0xA10F":"BlueSolar MPPT VE.Can 150/100", + "0xA112":"BlueSolar MPPT VE.Can 250/70", + "0xA113":"BlueSolar MPPT VE.Can 250/100", + "0xA114":"SmartSolar MPPT VE.Can 250/70 rev2", + "0xA115":"SmartSolar MPPT VE.Can 250/100 rev2", + "0xA116":"SmartSolar MPPT VE.Can 250/85 rev2", + "0xA117":"BlueSolar MPPT VE.Can 150/100 rev2", + "0xA201":"Phoenix Inverter 12V 250VA 230V", + "0xA202":"Phoenix Inverter 24V 250VA 230V", + "0xA204":"Phoenix Inverter 48V 250VA 230V", + "0xA211":"Phoenix Inverter 12V 375VA 230V", + "0xA212":"Phoenix Inverter 24V 375VA 230V", + "0xA214":"Phoenix Inverter 48V 375VA 230V", + "0xA221":"Phoenix Inverter 12V 500VA 230V", + "0xA222":"Phoenix Inverter 24V 500VA 230V", + "0xA224":"Phoenix Inverter 48V 500VA 230V", + "0xA231":"Phoenix Inverter 12V 250VA 230V", + "0xA232":"Phoenix Inverter 24V 250VA 230V", + "0xA234":"Phoenix Inverter 48V 250VA 230V", + "0xA239":"Phoenix Inverter 12V 250VA 120V", + "0xA23A":"Phoenix Inverter 24V 250VA 120V", + "0xA23C":"Phoenix Inverter 48V 250VA 120V", + "0xA241":"Phoenix Inverter 12V 375VA 230V", + "0xA242":"Phoenix Inverter 24V 375VA 230V", + "0xA244":"Phoenix Inverter 48V 375VA 230V", + "0xA249":"Phoenix Inverter 12V 375VA 120V", + "0xA24A":"Phoenix Inverter 24V 375VA 120V", + "0xA24C":"Phoenix Inverter 48V 375VA 120V", + "0xA251":"Phoenix Inverter 12V 500VA 230V", + "0xA252":"Phoenix Inverter 24V 500VA 230V", + "0xA254":"Phoenix Inverter 48V 500VA 230V", + "0xA259":"Phoenix Inverter 12V 500VA 120V", + "0xA25A":"Phoenix Inverter 24V 500VA 120V", + "0xA25C":"Phoenix Inverter 48V 500VA 120V", + "0xA261":"Phoenix Inverter 12V 800VA 230V", + "0xA262":"Phoenix Inverter 24V 800VA 230V", + "0xA264":"Phoenix Inverter 48V 800VA 230V", + "0xA269":"Phoenix Inverter 12V 800VA 120V", + "0xA26A":"Phoenix Inverter 24V 800VA 120V", + "0xA26C":"Phoenix Inverter 48V 800VA 120V", + "0xA271":"Phoenix Inverter 12V 1200VA 230V", + "0xA272":"Phoenix Inverter 24V 1200VA 230V", + "0xA274":"Phoenix Inverter 48V 1200VA 230V", + "0xA279":"Phoenix Inverter 12V 1200VA 120V", + "0xA27A":"Phoenix Inverter 24V 1200VA 120V", + "0xA27C":"Phoenix Inverter 48V 1200VA 120V", + "0xA281":"Phoenix Inverter 12V 1600VA 230V", + "0xA282":"Phoenix Inverter 24V 1600VA 230V", + "0xA284":"Phoenix Inverter 48V 1600VA 230V", + "0xA291":"Phoenix Inverter 12V 2000VA 230V", + "0xA292":"Phoenix Inverter 24V 2000VA 230V", + "0xA294":"Phoenix Inverter 48V 2000VA 230V", + "0xA2A1":"Phoenix Inverter 12V 3000VA 230V", + "0xA2A2":"Phoenix Inverter 24V 3000VA 230V", + "0xA2A4":"Phoenix Inverter 48V 3000VA 230V", + "0xA340":"Phoenix Smart IP43 Charger 12|50 (1+1)", + "0xA341":"Phoenix Smart IP43 Charger 12|50 (3)", + "0xA342":"Phoenix Smart IP43 Charger 24|25 (1+1)", + "0xA343":"Phoenix Smart IP43 Charger 24|25 (3)", + "0xA344":"Phoenix Smart IP43 Charger 12|30 (1+1)", + "0xA345":"Phoenix Smart IP43 Charger 12|30 (3)", + "0xA346":"Phoenix Smart IP43 Charger 24|16 (1+1)", + "0xA347":"Phoenix Smart IP43 Charger 24|16 (3)", + "0xA381":"BMV-712 Smart", + "0xA382":"BMV-710H Smart", + "0xA383":"BMV-712 Smart Rev2", + "0xA389":"SmartShunt 500A/50mV", + "0xA38A":"SmartShunt 1000A/50mV", + "0xA38B":"SmartShunt 2000A/50mV", + "0xA3F0":"Orion XS 12V/12V-50A", + "0xA3F1":"Orion XS 1400" +} diff --git a/public/i18n/de.json b/public/i18n/de.json new file mode 100644 index 0000000..14bc6fd --- /dev/null +++ b/public/i18n/de.json @@ -0,0 +1,22 @@ +{ + "datafield_CS": "Systemstatus", + "datafield_ERR": "Fehler", + "datafield_FW": "Firmwareversion", + "datafield_FWE": "Firmwareversion", + "datafield_H19": "Gesamtertrag seit letztem Reset", + "datafield_H20": "Ertrag aktuellster Tag", + "datafield_H21": "max. Leistung aktuellster Tag", + "datafield_H22": "Ertrag Vortag", + "datafield_H23": "max. Leistung Vortag", + "datafield_I": "Stromstärke Kanal 1", + "datafield_LOAD": "Status Lastausgang", + "datafield_MPPT": "Arbeitsmodus Tracker", + "datafield_OR": "Abschaltgrund", + "datafield_P": "Systemleistung", + "datafield_PPV": "Panelleistung", + "datafield_Relay": "Status Relais", + "datafield_V": "Spannung Kanal 1", + "datafield_VPV": "Panelspannung", + "latestDataPoint": "letzter Datenpunkt", + "serialNumber": "Seriennummer" +} diff --git a/public/i18n/en.json b/public/i18n/en.json new file mode 100644 index 0000000..4df9909 --- /dev/null +++ b/public/i18n/en.json @@ -0,0 +1,22 @@ +{ + "datafield_CS": "state of operation", + "datafield_ERR": "error", + "datafield_FW": "firmware version", + "datafield_FWE": "firmware version", + "datafield_H19": "total yield since last reset", + "datafield_H20": "yield latest day", + "datafield_H21": "max. power latest day", + "datafield_H22": "yield yesterday", + "datafield_H23": "max. power yesterday", + "datafield_I": "current channel 1", + "datafield_LOAD": "status load output", + "datafield_MPPT": "tracker operation mode", + "datafield_OR": "off reason", + "datafield_P": "system power", + "datafield_PPV": "panel power", + "datafield_Relay": "relay status", + "datafield_V": "voltage channel 1", + "datafield_VPV": "panel voltage", + "latestDataPoint": "last data point", + "serialNumber": "serial number" +} diff --git a/public/images/README.md b/public/images/README.md new file mode 100644 index 0000000..d8e1ac4 --- /dev/null +++ b/public/images/README.md @@ -0,0 +1,10 @@ +# Product images + +Put any picture of your own products in this folder. It might be in JPEG or PNG format. + +## Naming convention + +Every picture needs to be named in a certain way to be automatically identified by the frontend. +The naming shall follow this convention: +ProductID.jpg or ProductID.png +Let's say you have a SmartSolar MPPT VE.CAN150/70 rev. 2 (like the developer of this tool). The product ID would be 0xA10C. So any picture shall be named 0xA10C.jpg or 0xA10C.png diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..fab8c19 --- /dev/null +++ b/public/index.html @@ -0,0 +1,29 @@ + + + + VE.Direct web-UI + + + + + +
+
+
🏠 Dashboard
+
+ +
+

Dashboard

+
+
+ +

Details

+
+
+
+ + + + diff --git a/public/main.css b/public/main.css new file mode 100644 index 0000000..84c344b --- /dev/null +++ b/public/main.css @@ -0,0 +1,36 @@ +/* + * main.css + * + * This file is part of the VE.Direct web UI + * https://git.schauaus.at/VE-Tools/VE.Direct-webui + * It is licensed under the GPL3.0 or later. + * You should have received a copy of the license with this software + */ +a, a.visited { color: #aa0; font-weight: bold; } + +body { font: normal 16px sans-serif; color: #ccc; background-color: #222; } +.bright { background-color: #ddd; color: #444; } + +.container { display: flex; width: 100%; } + +.dashboard { display: inline-block; } +.deviceDetails { display: none; } +#deviceDetailsProductImage { max-width: 200px; max-height: 200px; } + +.dark { background-color: #222; } + +footer { font-size: 12px; text-align: center; clear: both; } + +h1 { display: inline-block; } + +img { float: left; padding: 0px; margin: 0px; margin-right: 5px; } + +.mainView { min-width: 600px; background-color: #660; margin: 5px; flex-grow: 1; } + +.productImageSmall { height: 50px; } +.productList { width: 300px; overflow: auto; background-color: #00a; margin: 5px; } +.padding { padding: 5px; } + +table { border-collapse: collapse; border-width: 0px; } + +[id^="li"] { cursor: pointer; } diff --git a/public/main.js b/public/main.js new file mode 100644 index 0000000..c64bf6c --- /dev/null +++ b/public/main.js @@ -0,0 +1,243 @@ +/* + * main.js + * + * This file is part of the VE.Direct web UI + * https://git.schauaus.at/VE-Tools/VE.Direct-webui + * It is licensed under the GPL3.0 or later. + * You should have received a copy of the license with this software + */ + +var deviceDataParsed = false +var deviceDataSet = {} +var deviceDataTimer; +var deviceIds = {} +var devices = []; +var deviceTimer; +var i18n = {} +var lang = 'de' + +function checkFile(url) { + let xhr = new XMLHttpRequest() + xhr.open('HEAD', url, false); + xhr.send() + return xhr.status !==404 +} + +function fileReadJson(ReturnFunction, url, params = "") { + let xhr = new XMLHttpRequest() + xhr.onload = function () { + if(xhr.status == 200) + ReturnFunction(xhr.response) + else return false + } + xhr.onerror = function() { return false } + xhr.open('POST', url, true) + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.responseType = 'json' + xhr.send(params) +} + +function getDeviceData(device) { + if(!device.startsWith('0x')) //check if device ID is valid + return false + fileReadJson(parseDeviceData, './dbQuery.php', 'q=deviceData&device=' + device) +} + +function getDeviceIds() { + let xhr = new XMLHttpRequest() + xhr.onload = function() { + if(xhr.status === 200) + deviceIds = xhr.response + } + xhr.open('GET', './deviceIds.json', true) + xhr.responseType = 'json' + xhr.send() +} + +function getDevices() { + if(Object.keys(deviceIds).length < 1) return + if(Object.keys(i18n).lenght < 1) return + clearInterval(deviceTimer) + let xhr = new XMLHttpRequest() + xhr.onload = function() { + if(xhr.status != 200) { // error handling + devices = false + } + else { //process data + devices = [] + for(let i=0; i' + block.addEventListener("click", showDevice) + } +} + +function showDevice(event) { + let deviceName = event.currentTarget.id.substr(2) + if(deviceName == "Dashboard") { + document.getElementById('deviceDetails').style.display = "none" //hide device details + document.getElementById('dashboard').style.display = "inline-block" + } + else { + document.getElementById('dashboard').style.display = "none" //hide dashboard + + //cleanup device details + let deviceDetailNode = document.getElementById('deviceDetails') + while(deviceDetailNode.children.length > 3) + deviceDetailNode.removeChild( deviceDetailNode.children[ deviceDetailNode.children.length - 1 ] ) + + //show device data block and fill in the details + document.getElementById('deviceDetails').style.display = "inline-block" + renderDeviceDetails(deviceName) + } +} + +function showDeviceData() { + if(!deviceDataParsed) return + clearInterval(deviceDataTimer) + + //parse years, months, days and timestamps + let years = Object.keys(deviceDataSet) + years.sort() + let latestYear = years[years.length - 1] + let months = Object.keys(deviceDataSet[latestYear]) + months.sort() + let latestMonth = months[months.length - 1] + let days = Object.keys(deviceDataSet[latestYear][latestMonth]) + days.sort() + let latestDay = days[days.length - 1] + let timestamps = Object.keys( deviceDataSet[latestYear][latestMonth][latestDay] ) + timestamps.sort() + let latestTimestamp = timestamps[timestamps.length - 1] + let latestDataPoint = document.createElement('div') + latestDataPoint.innerHTML = i18n[lang]['latestDataPoint'] + ': ' + latestYear + '-' + latestMonth + '-' + latestDay + ' ' + latestTimestamp + document.getElementById('deviceDetails').appendChild(latestDataPoint) + + //parse latest datapoint regarding system power, system voltage, solar power, solar voltage + let latestDataSet = deviceDataSet[latestYear][latestMonth][latestDay][latestTimestamp] + let latestPowerData = document.createElement('table') + if(latestDataSet['PPV']) + latestPowerData.innerHTML += '' + i18n[lang]['datafield_PPV'] + ': ' + latestDataSet['PPV'] + 'W' + '' + if(latestDataSet['VPV']) + latestPowerData.innerHTML += '' + i18n[lang]['datafield_VPV'] + ': ' + parseInt(latestDataSet['VPV']) / 1000 + 'V' + '' + if(latestDataSet['V']) + latestPowerData.innerHTML += '' + i18n[lang]['datafield_V'] + ': ' + parseInt(latestDataSet['V']) / 1000 + 'V' + '' + if(latestDataSet['I']) + latestPowerData.innerHTML += '' + i18n[lang]['datafield_I'] + ': ' + parseInt(latestDataSet['I']) / 1000 + 'A' + '' + if(latestDataSet['P']) + latestPowerData.innerHTML += '' + i18n[lang]['datafield_P'] + ': ' + latestDataSet['P'] + 'W' + '' + if(latestDataSet['H20']) + latestPowerData.innerHTML += '' + i18n[lang]['datafield_H20'] + ': ' + parseInt(latestDataSet['H20']) * 0.01 + 'kWh' + '' + if(latestDataSet['H22']) + latestPowerData.innerHTML += '' + i18n[lang]['datafield_H22'] + ': ' + parseInt(latestDataSet['H22']) / 100 + 'kWh' + '' + if(latestDataSet['H19']) + latestPowerData.innerHTML += '' + i18n[lang]['datafield_H19'] + ': ' + parseInt(latestDataSet['H19']) / 100 + 'kWh' + '' + if(latestDataSet['H21']) + latestPowerData.innerHTML += '' + i18n[lang]['datafield_H21'] + ': ' + parseInt(latestDataSet['H21']) + 'W' + '' + if(latestDataSet['H23']) + latestPowerData.innerHTML += '' + i18n[lang]['datafield_H23'] + ': ' + parseInt(latestDataSet['H23']) + 'W' + '' + + document.getElementById('deviceDetails').appendChild(latestPowerData) +}