parent
839f6aca33
commit
3d3d8b42e2
28 changed files with 13755 additions and 18355 deletions
13
.eslintrc.js
Normal file
13
.eslintrc.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/essential",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier",
|
||||
],
|
||||
};
|
||||
3
.prettierrc.js
Normal file
3
.prettierrc.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
printWidth: 100,
|
||||
};
|
||||
1
Makefile
1
Makefile
|
|
@ -21,6 +21,7 @@ install:
|
|||
|
||||
lint:
|
||||
golangci-lint run
|
||||
npm run lint
|
||||
|
||||
test:
|
||||
@echo "Running testsuite"
|
||||
|
|
|
|||
|
|
@ -1,26 +1,29 @@
|
|||
/* reverse loading animation */
|
||||
.progress-bar-animated {
|
||||
animation-direction: reverse;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.btn.caption {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
.btn.first {
|
||||
border-top-left-radius: .2rem;
|
||||
border-bottom-left-radius: .2rem;
|
||||
border-top-left-radius: 0.2rem;
|
||||
border-bottom-left-radius: 0.2rem;
|
||||
}
|
||||
code {
|
||||
font-size: 87.5%;
|
||||
color: #e83e8c;
|
||||
word-wrap: break-word;
|
||||
font-size: 87.5%;
|
||||
color: #e83e8c;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.value, input[type=radio], label.btn {
|
||||
white-space: nowrap !important;
|
||||
.value,
|
||||
input[type="radio"],
|
||||
label.btn {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.text-muted a, .text-muted a:hover {
|
||||
color: #6c757d!important;
|
||||
text-decoration: none;
|
||||
.text-muted a,
|
||||
.text-muted a:hover {
|
||||
color: #6c757d !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
.bg-muted {
|
||||
opacity: 0.25;
|
||||
}
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,58 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 576 512"
|
||||
version="1.1"
|
||||
id="svg12"
|
||||
sodipodi:docname="leaf.svg"
|
||||
inkscape:version="1.0beta2 (2b71d25, 2019-12-03)">
|
||||
<metadata
|
||||
id="metadata18">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs16" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
inkscape:document-rotation="0"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1280"
|
||||
inkscape:window-height="755"
|
||||
id="namedview14"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.92768596"
|
||||
inkscape:cx="353.312"
|
||||
inkscape:cy="200.09826"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg12" />
|
||||
<path
|
||||
style="stroke-width:1.1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M546.2 9.7c-5.6-12.5-21.6-13-28.3-1.2C486.9 62.4 431.4 96 368 96h-80C182 96 96 182 96 288c0 7 .8 13.7 1.5 20.5C161.3 262.8 253.4 224 384 224c8.8 0 16 7.2 16 16s-7.2 16-16 16C132.6 256 26 410.1 2.4 468c-6.6 16.3 1.2 34.9 17.5 41.6 16.4 6.8 35-1.1 41.8-17.3 1.5-3.6 20.9-47.9 71.9-90.6 32.4 43.9 94 85.8 174.9 77.2C465.5 467.5 576 326.7 576 154.3c0-50.2-10.8-102.2-29.8-144.6z"
|
||||
id="path10" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path20"
|
||||
d="m 21.21449,509.39751 c -5.971286,-2.52059 -9.269565,-4.81794 -12.9733928,-9.03637 -6.1766191,-7.03478 -8.8842152,-17.3518 -6.971629,-26.56467 1.258445,-6.0619 11.8107088,-27.39624 19.8175918,-40.06678 22.713423,-35.94297 56.773767,-71.44959 94.11207,-98.10823 68.76834,-49.09895 154.92997,-76.24193 249.47048,-78.5892 11.23532,-0.27896 21.89988,-0.8765 23.69902,-1.32786 4.02501,-1.0098 9.1941,-5.41782 10.76663,-9.18141 2.92101,-6.99096 0.29781,-15.90466 -5.87906,-19.97721 l -3.71288,-2.44799 -20.91224,0.12879 c -95.91234,0.59069 -191.38366,28.82528 -262.4752,77.62416 -4.25115,2.91809 -7.89194,5.14304 -8.090638,4.94433 -1.034492,-1.03449 -1.681776,-19.91176 -1.047227,-30.54108 4.946935,-82.86599 63.514115,-153.47981 144.215265,-173.87878 19.41459,-4.90746 20.47839,-4.979088 83.88455,-5.648034 49.15213,-0.518563 60.64802,-0.871339 68.23769,-2.09402 19.39657,-3.124747 35.56867,-8.269165 52.1562,-16.591125 29.26858,-14.684035 50.80653,-34.823142 71.56328,-66.915381 3.64477,-5.6352221 5.2829,-7.343539 8.63712,-9.0071805 5.61954,-2.78721054 10.73552,-2.07507421 15.40609,2.1445063 3.91499,3.5369576 8.97418,14.4546312 15.06776,32.5161072 20.64772,61.200058 24.79308,128.916947 11.83935,193.402597 -25.37559,126.32372 -112.033,220.27326 -224.43817,243.32443 -17.6222,3.61382 -29.64707,4.91447 -50.19768,5.42955 -21.4123,0.53668 -32.33232,-0.45528 -49.11375,-4.46143 -40.56718,-9.6844 -78.7741,-33.83895 -104.95018,-66.34986 l -5.80657,-7.21182 -7.6437,6.87084 c -26.141169,23.49803 -48.851538,52.36799 -61.996828,78.81184 -2.979862,5.99446 -6.952342,12.64221 -8.827734,14.77277 -7.841316,8.90821 -23.148129,12.54014 -33.836218,8.02851 z"
|
||||
style="fill:#007bff;stroke:#007bff;stroke-width:1.1;fill-opacity:1;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
</svg>
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" viewBox="0 0 576 512" version="1.1" id="svg12"
|
||||
sodipodi:docname="leaf.svg" inkscape:version="1.0beta2 (2b71d25, 2019-12-03)">
|
||||
<metadata id="metadata18">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs id="defs16" />
|
||||
<sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" inkscape:document-rotation="0" borderopacity="1"
|
||||
objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2"
|
||||
inkscape:window-width="1280" inkscape:window-height="755" id="namedview14" showgrid="false"
|
||||
inkscape:zoom="0.92768596" inkscape:cx="353.312" inkscape:cy="200.09826" inkscape:window-x="0"
|
||||
inkscape:window-y="23" inkscape:window-maximized="0" inkscape:current-layer="svg12" />
|
||||
<path style="stroke-width:1.1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M546.2 9.7c-5.6-12.5-21.6-13-28.3-1.2C486.9 62.4 431.4 96 368 96h-80C182 96 96 182 96 288c0 7 .8 13.7 1.5 20.5C161.3 262.8 253.4 224 384 224c8.8 0 16 7.2 16 16s-7.2 16-16 16C132.6 256 26 410.1 2.4 468c-6.6 16.3 1.2 34.9 17.5 41.6 16.4 6.8 35-1.1 41.8-17.3 1.5-3.6 20.9-47.9 71.9-90.6 32.4 43.9 94 85.8 174.9 77.2C465.5 467.5 576 326.7 576 154.3c0-50.2-10.8-102.2-29.8-144.6z"
|
||||
id="path10" />
|
||||
<path inkscape:connector-curvature="0" id="path20"
|
||||
d="m 21.21449,509.39751 c -5.971286,-2.52059 -9.269565,-4.81794 -12.9733928,-9.03637 -6.1766191,-7.03478 -8.8842152,-17.3518 -6.971629,-26.56467 1.258445,-6.0619 11.8107088,-27.39624 19.8175918,-40.06678 22.713423,-35.94297 56.773767,-71.44959 94.11207,-98.10823 68.76834,-49.09895 154.92997,-76.24193 249.47048,-78.5892 11.23532,-0.27896 21.89988,-0.8765 23.69902,-1.32786 4.02501,-1.0098 9.1941,-5.41782 10.76663,-9.18141 2.92101,-6.99096 0.29781,-15.90466 -5.87906,-19.97721 l -3.71288,-2.44799 -20.91224,0.12879 c -95.91234,0.59069 -191.38366,28.82528 -262.4752,77.62416 -4.25115,2.91809 -7.89194,5.14304 -8.090638,4.94433 -1.034492,-1.03449 -1.681776,-19.91176 -1.047227,-30.54108 4.946935,-82.86599 63.514115,-153.47981 144.215265,-173.87878 19.41459,-4.90746 20.47839,-4.979088 83.88455,-5.648034 49.15213,-0.518563 60.64802,-0.871339 68.23769,-2.09402 19.39657,-3.124747 35.56867,-8.269165 52.1562,-16.591125 29.26858,-14.684035 50.80653,-34.823142 71.56328,-66.915381 3.64477,-5.6352221 5.2829,-7.343539 8.63712,-9.0071805 5.61954,-2.78721054 10.73552,-2.07507421 15.40609,2.1445063 3.91499,3.5369576 8.97418,14.4546312 15.06776,32.5161072 20.64772,61.200058 24.79308,128.916947 11.83935,193.402597 -25.37559,126.32372 -112.033,220.27326 -224.43817,243.32443 -17.6222,3.61382 -29.64707,4.91447 -50.19768,5.42955 -21.4123,0.53668 -32.33232,-0.45528 -49.11375,-4.46143 -40.56718,-9.6844 -78.7741,-33.83895 -104.95018,-66.34986 l -5.80657,-7.21182 -7.6437,6.87084 c -26.141169,23.49803 -48.851538,52.36799 -61.996828,78.81184 -2.979862,5.99446 -6.952342,12.64221 -8.827734,14.77277 -7.841316,8.90821 -23.148129,12.54014 -33.836218,8.02851 z"
|
||||
style="fill:#007bff;stroke:#007bff;stroke-width:1.1;fill-opacity:1;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.6 KiB |
|
|
@ -1,15 +1,12 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="576.000000pt" height="576.000000pt" viewBox="0 0 576.000000 576.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,576.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M5254 5426 c-30 -14 -94 -84 -94 -105 0 -6 -4 -11 -10 -11 -5 0 -10
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="576.000000pt" height="576.000000pt"
|
||||
viewBox="0 0 576.000000 576.000000" preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,576.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none">
|
||||
<path d="M5254 5426 c-30 -14 -94 -84 -94 -105 0 -6 -4 -11 -10 -11 -5 0 -10
|
||||
-4 -10 -10 0 -9 -64 -106 -80 -120 -3 -3 -21 -25 -40 -50 -147 -191 -344 -351
|
||||
-572 -466 -70 -35 -236 -103 -268 -109 -8 -2 -35 -9 -60 -16 -53 -15 -54 -15
|
||||
-100 -23 -19 -3 -46 -8 -60 -11 -55 -12 -115 -16 -330 -20 -124 -2 -227 -5
|
||||
|
|
@ -45,6 +42,6 @@ fill="#000000" stroke="none">
|
|||
11 51 3 14 8 36 10 50 2 14 6 36 9 50 6 34 16 97 21 140 3 19 7 55 10 79 16
|
||||
121 19 198 19 446 -1 238 -4 308 -20 430 -2 17 -5 49 -8 72 -2 23 -7 55 -10
|
||||
70 -3 15 -8 48 -12 73 -3 24 -7 48 -10 51 -2 4 -6 24 -10 45 -19 130 -124 484
|
||||
-181 614 -57 130 -81 162 -130 181 -34 13 -82 11 -119 -5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
-181 614 -57 130 -81 162 -130 181 -34 13 -82 11 -119 -5z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"start_url": "../#",
|
||||
"name": "EV Charge Controller",
|
||||
"short_name": "EV Charge Controller",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
"name": "EV Charge Controller",
|
||||
"short_name": "EV Charge Controller",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,507 +1,55 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="EV Charge Controller">
|
||||
<meta name="author" content="andig">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="description" content="EV Charge Controller" />
|
||||
<meta name="author" content="andig" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="ico/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png">
|
||||
<link rel="manifest" href="ico/site.webmanifest">
|
||||
<link rel="mask-icon" href="ico/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="shortcut icon" href="ico/favicon.ico">
|
||||
<meta name="msapplication-TileColor" content="#2b5797">
|
||||
<meta name="msapplication-config" content="ico/browserconfig.xml">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="ico/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png" />
|
||||
<link rel="manifest" href="ico/site.webmanifest" />
|
||||
<link rel="mask-icon" href="ico/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<link rel="shortcut icon" href="ico/favicon.ico" />
|
||||
<meta name="msapplication-TileColor" content="#2b5797" />
|
||||
<meta name="msapplication-config" content="ico/browserconfig.xml" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
<title>evcc</title>
|
||||
<title>evcc</title>
|
||||
|
||||
<!-- CSS -->
|
||||
<link href="../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="./css/app.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<!-- CSS -->
|
||||
<link href="../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="./css/app.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.evcc = {
|
||||
version: "[[.Version]]",
|
||||
configured: "[[.Configured]]",
|
||||
};
|
||||
</script>
|
||||
|
||||
<div id="app">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="https://github.com/andig/evcc"><fa-icon icon="leaf" class="text-primary mr-2"></fa-icon>evcc</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup"
|
||||
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
|
||||
<div class="navbar-nav">
|
||||
<router-link class="nav-item nav-link pb-1" to="/">Laden</router-link>
|
||||
<router-link class="nav-item nav-link pb-1" to="/config">Konfiguration</router-link>
|
||||
<a class="nav-item nav-link pb-1" href="https://github.com/andig/evcc/issues" target="_blank">Support</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="app"></div>
|
||||
<div id="toasts"></div>
|
||||
|
||||
<version installed="[[.Version]]" id="version-bar"></version>
|
||||
|
||||
<message-toasts></message-toasts>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
|
||||
<script type="text/x-template" id="version-template">
|
||||
<div class="collapse" ref="bar">
|
||||
<div class="row p-3 bg-warning">
|
||||
<div class="col-12">
|
||||
Neue Version verfügbar!
|
||||
Installiert: {{installed}}.
|
||||
Verfügbar: {{state.availableVersion}}.
|
||||
<b class="px-3" data-toggle="collapse" data-target="#release-notes" v-if="state.releaseNotes">
|
||||
<a href="#" class="text-body">
|
||||
Release notes
|
||||
<fa-icon icon="chevron-up" v-if="notesShown"></fa-icon>
|
||||
<fa-icon icon="chevron-down" v-else></fa-icon>
|
||||
</a>
|
||||
</b>
|
||||
<b class="px-3">
|
||||
<a v-bind:href="'https://github.com/andig/evcc/releases/tag/'+state.availableVersion" class="text-body">
|
||||
Download <fa-icon icon="chevron-down"></fa-icon>
|
||||
</a>
|
||||
</b>
|
||||
<button type="button" class="close float-right" style="margin-top: -2px" aria-label="Close" data-toggle="collapse" data-target="#version-bar">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row p-3 bg-light collapse" id="release-notes" ref="notes">
|
||||
<div class="col-12" v-html="state.releaseNotes"></div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<div id="toasts">
|
||||
<div aria-atomic="true" style="position: absolute; top: 4rem; right: 0.5rem;">
|
||||
<!-- Position it -->
|
||||
<message-toast v-for="item in items" v-bind:item="item" :id="'message-id-'+item.id" :key="item.id">
|
||||
</message-toast>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/x-template" id="message-template">
|
||||
<div class="toast" data-delay="10000" v-bind:data-autohide="true">
|
||||
<div class="toast-header">
|
||||
<strong class="mr-auto" v-if="item.type != 'warn'"><fa-icon class="text-danger" icon="exclamation-triangle"></fa-icon> Error</strong>
|
||||
<strong class="mr-auto" v-if="item.type == 'warn'"><fa-icon class="text-warning" icon="exclamation-triangle"></fa-icon> Warning</strong>
|
||||
<small v-if="item.status">HTTP {{item.status}}</small>
|
||||
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="toast-body">{{item.message}}</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="main-template">
|
||||
<div class="container">
|
||||
<site v-bind:state="state" v-if="configured('[[.Configured]]')"></site>
|
||||
<div v-else>
|
||||
<div class="row py-5">
|
||||
<div class="col12">
|
||||
<p class="h1 pt-5 pb-2 border-bottom">Willkommen bei evcc</p>
|
||||
<p class="lead pt-2"><b>evcc</b> ist dient zur flexiblen Ladesteuerung von Elektrofahrzeugen.</p>
|
||||
<p class="pt-2"> Es sieht aus, als wäre Dein <b>evcc</b> noch nicht konfiguriert. Um <b>evcc</b> zu
|
||||
konfigurieren sind die folgenden Schritte notwendig:
|
||||
<ol class="pt-2">
|
||||
<li>Erzeugen einer Konfigurationsdatei mit Namen <code>evcc.yaml</code>. Die
|
||||
Standardkonfiguration <code>evcc.dist.yaml</code> kann dafür als Vorlage dienen (<a
|
||||
href="https://github.com/andig/evcc/blob/master/evcc.dist.yaml">Download</a>).</li>
|
||||
<li>Konfiguration der Wallbox als <code>chargers</code>.</li>
|
||||
<li>Konfiguration des EVU Zählers und evtl. weiterer Zähler unter <code>meters</code>.</li>
|
||||
<li>Konfiguration des Netzanschlusses unter <code>site</code>. In einer Site wird der Netzanschluss mit
|
||||
dem konfigurierten EVU Zähler (<code>meter</code>) verbunden.</li>
|
||||
<li>Konfiguration eines Ladepunktes unter <code>loadpoints</code>. In einem Ladepunkt wird die
|
||||
konfigurierte Wallbox (<code>charger</code>) mit dem Ladepunkt verbunden.</li>
|
||||
<li>Start von <b>evcc</b> mit der neu erstellten Konfiguration: <code>evcc -c evcc.yaml</code></li>
|
||||
</ol>
|
||||
</p>
|
||||
<p>Minimale Beispielkonfiguration für <b>evcc</b>:</p>
|
||||
<p>
|
||||
<code><pre class="mx-3">
|
||||
uri: localhost:7070 # Adresse für UI
|
||||
interval: 10s # Regelintervall
|
||||
meters:
|
||||
- name: evu-zähler
|
||||
type: ... # Detailkonfiguration des EVU Zählers
|
||||
- name: ladezähler
|
||||
type: ... # Detailkonfiguration des Ladezählers (optional)
|
||||
chargers:
|
||||
- name: wallbox
|
||||
type: ... # Detailkonfiguration der Wallbox
|
||||
site:
|
||||
title: Home
|
||||
meters:
|
||||
grid: evu-zähler # EVU Zähler
|
||||
loadpoints:
|
||||
- title: Ladepunkt # ui display name
|
||||
charger: wallbox # charger
|
||||
meters:
|
||||
charge: ladezähler # Ladezählers (optional)
|
||||
</pre></code>
|
||||
</p>
|
||||
<p>Viel Spass mit <b>evcc</b>! Bei Problemen kannst Du uns auf <a href="https://github.com/andig/evcc/issues">GitHub</a> erreichen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="site-template">
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="d-none d-md-flex col-12 col-md-4 mt-md-4 align-items-end">
|
||||
<p class="h1">{{state.title||"Home"}}</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 mt-md-4" v-if="multi">
|
||||
<site-details v-bind:state="state"></site-details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-none d-md-flex border-bottom"></div>
|
||||
|
||||
<div class="row" v-if="!multi">
|
||||
<div class="d-none d-md-block col-md-4"></div>
|
||||
<div class="col-12 col-md-8">
|
||||
<site-details v-bind:state="state"></site-details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<loadpoint v-for="(loadpoint,id) in state.loadpoints"
|
||||
v-bind:id="id" :key="id" :state="loadpoint" :pv="state.gridConfigured" :multi="multi">
|
||||
</loadpoint>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="site-details-template">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.gridConfigured">
|
||||
<div class="mb-2 value" v-if="state.gridPower > 0">
|
||||
Bezug <fa-icon icon="arrow-down" class="text-primary"></fa-icon>
|
||||
</div>
|
||||
<div class="mb-2 value" v-else>
|
||||
Einspeisung <fa-icon icon="arrow-up" class="text-primary"></fa-icon>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
{{fmt(state.gridPower)}} <small class="text-muted">{{fmtUnit(state.gridPower)}}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.pvConfigured">
|
||||
<div class="mb-2 value">
|
||||
Erzeugung <fa-icon icon="sun" v-bind:class="{'text-primary':state.pvPower<0,'text-muted':state.pvPower>=0}"></fa-icon>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
{{fmt(state.pvPower)}} <small class="text-muted">{{fmtUnit(state.pvPower)}}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-md-block col-6 col-md-3 mt-3" v-if="state.batteryConfigured">
|
||||
<div class="mb-2 value">Batterie
|
||||
<fa-icon class="text-primary" icon="arrow-down" v-if="state.batteryPower < 0"></fa-icon>
|
||||
<fa-icon class="text-primary" icon="arrow-up" v-if="state.batteryPower > 0"></fa-icon>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
{{fmt(state.batteryPower)}} <small class="text-muted">{{fmtUnit(state.batteryPower)}}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.batteryConfigured">
|
||||
<div class="mb-2 value">SoC
|
||||
<fa-icon class="text-primary" icon="battery-three-quarters" v-bind:class="{'text-primary':state.batteryPower>0,'text-muted':state.batteryPower<0}"></fa-icon>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
{{fmt(state.batterySoC)}} <small class="text-muted">%</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="loadpoint-template">
|
||||
<div>
|
||||
<div class="row" v-if="multi">
|
||||
<div class="col-12 col-md-4 d-md-flex mt-3 mt-md-5 align-items-end">
|
||||
<span class="h1 align-bottom">{{state.title||"Ladepunkt"}}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-8 d-none d-md-block mt-3 mt-md-5">
|
||||
<loadpoint-details v-bind:state="state"></loadpoint-details>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-md-none">
|
||||
<div class="row mt-3 pb-3 bg-light">
|
||||
<div class="col-12 mt-3">
|
||||
<mode class="w-100" v-bind:mode="state.mode" :pv="pv" v-on:updated="targetMode"></mode>
|
||||
</div>
|
||||
<div class="col-12 mt-3" v-if="hasTargetSoC">
|
||||
<soc class="w-100" v-bind:soc="state.targetSoC" :levels="state.socLevels" v-on:updated="targetSoC"></soc>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-none d-md-flex mt-5 py-3 pb-4 text-center bg-light" v-if="!multi">
|
||||
<div class="mt-3" v-bind:class="{'col-md-6':hasTargetSoC,'col-md-12':!hasTargetSoC}">
|
||||
<mode v-bind:mode="state.mode" :pv="pv" :caption="true" v-on:updated="targetMode"></mode>
|
||||
</div>
|
||||
<div class="col-md-6 mt-3" v-if="hasTargetSoC">
|
||||
<soc v-bind:soc="state.targetSoC" :levels="state.socLevels" :caption="true" v-on:updated="targetSoC"></soc>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-md-none mt-2 pb-3 bg-light" v-if="!multi">
|
||||
<div class="col-12 mt-3">
|
||||
<mode class="w-100" v-bind:mode="state.mode" :pv="pv" v-on:updated="targetMode"></mode>
|
||||
</div>
|
||||
<div class="col-12 mt-3" v-if="hasTargetSoC">
|
||||
<soc class="w-100" v-bind:soc="state.targetSoC" :levels="state.socLevels" v-on:updated="targetSoC"></soc>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="!multi">
|
||||
<div class="col-12 col-md-4 d-none d-md-flex mt-3 mt-md-5">
|
||||
<span class="h1 ">{{state.title||"Ladepunkt"}}</span>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 d-flex d-md-flex mt-3 mt-md-5 pt-3"
|
||||
v-if="state.remoteDisabled">
|
||||
<h5 class="w-100">
|
||||
<span class="badge badge-warning w-100" v-if="state.remoteDisabled == 'soft'">
|
||||
{{state.remoteDisabledSource}}: Adaptives PV-Laden deaktiviert
|
||||
</span>
|
||||
<span class="badge badge-danger w-100" v-if="state.remoteDisabled == 'hard'">
|
||||
{{state.remoteDisabledSource}}: Deaktiviert
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row border-bottom d-none d-md-block"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 mt-3 mb-3 mb-md-0">
|
||||
<vehicle v-bind:state="state"></vehicle>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 d-none d-md-block mt-3" v-if="multi">
|
||||
<div class="mb-2">Modus</div>
|
||||
<mode class="btn-group-sm" v-bind:mode="state.mode" :pv="pv" v-on:updated="targetMode"></mode>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 d-none d-md-block mt-3" v-if="multi && hasTargetSoC">
|
||||
<div class="mb-2">Ladeziel</div>
|
||||
<soc class="btn-group-sm" v-bind:soc="state.targetSoC" :levels="state.socLevels" v-on:updated="targetSoC"></soc>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 d-none d-md-block" v-if="!multi">
|
||||
<loadpoint-details v-bind:state="state"></loadpoint-details>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-md-none">
|
||||
<loadpoint-details v-bind:state="state"></loadpoint-details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="loadpoint-details-template">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-3 mt-3">
|
||||
<div class="mb-2 value">Leistung
|
||||
<fa-icon class="text-primary ml-1" icon="temperature-low" v-if="state.climater=='heating'"></fa-icon>
|
||||
<fa-icon class="text-primary ml-1" icon="temperature-high" v-else-if="state.climater=='cooling'"></fa-icon>
|
||||
<fa-icon class="text-primary ml-1" icon="thermometer-half" v-else-if="state.climater=='on'"></fa-icon>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
{{fmt(state.chargePower)}} <small class="text-muted">{{fmtUnit(state.chargePower)}}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 mt-3">
|
||||
<div class="mb-2 value">Geladen</div>
|
||||
<h2 class="value">
|
||||
{{fmt(state.chargedEnergy)}} <small class="text-muted">{{fmtUnit(state.chargedEnergy)}}Wh</small>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3 mt-3">
|
||||
<div class="mb-2 value">Dauer</div>
|
||||
<h2 class="value">
|
||||
{{fmtShortDuration(state.chargeDuration)}} <small
|
||||
class="text-muted">{{fmtShortDurationUnit(state.chargeDuration)}}</small>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.soc">
|
||||
<div class="mb-2 value">Restzeit</div>
|
||||
<h2 class="value">
|
||||
{{fmtShortDuration(state.chargeEstimate)}} <small
|
||||
class="text-muted">{{fmtShortDurationUnit(state.chargeEstimate)}}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="mode-template">
|
||||
<div class="btn-group btn-group-toggle bg-white shadow-none">
|
||||
<label class="btn btn-outline-primary value disabled caption font-weight-bold" v-if="caption">
|
||||
Modus
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-bind:class="{active:mode=='off',first:!caption}">
|
||||
<input type="radio" value="off" v-on:click="targetMode('off')">Stop
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-bind:class="{active:mode=='now'}">
|
||||
<input type="radio" value="now" v-on:click="targetMode('now')">Sofort
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-bind:class="{active:mode=='minpv'}" v-if="pv">
|
||||
<input type="radio" value="minpv" v-on:click="targetMode('minpv')">
|
||||
<span class="d-inline d-lg-none">Min</span>
|
||||
<span class="d-none d-lg-inline">Min + PV</span>
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-bind:class="{active:mode=='pv'}" v-if="pv">
|
||||
<input type="radio" value="pv" v-on:click="targetMode('pv')">
|
||||
<span class="d-inline d-md-none">PV</span>
|
||||
<span class="d-none d-md-inline">Nur PV</span>
|
||||
</label>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="soc-template">
|
||||
<div class="btn-group btn-group-toggle bg-white shadow-none">
|
||||
<label class="btn btn-outline-primary disabled caption font-weight-bold" v-if="caption">
|
||||
Ladeziel
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-for="(level,id) in levelsOrDefault" v-bind:level="level" :id="id"
|
||||
:class="{active:soc==level,first:!caption&&id==0}">
|
||||
<input type="radio" v-bind:value="level" v-on:click="targetSoC(level)">{{level}}%
|
||||
</label>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="vehicle-template">
|
||||
<div>
|
||||
<div class="mb-2">{{state.socTitle||"Fahrzeug"}}</div>
|
||||
<div class="progress" style="height: 24px; font-size:100%; margin-top:16px">
|
||||
<div class="progress-bar" role="progressbar" v-bind:class="{
|
||||
'progress-bar-striped': state.charging,
|
||||
'progress-bar-animated': state.charging,
|
||||
'bg-light': !state.connected,
|
||||
'text-secondary': !state.connected,
|
||||
'bg-warning': state.connected && minSoCActive,
|
||||
}" v-bind:style="{width: socChargeDisplayWidth+'%'}">{{socChargeDisplayValue}}
|
||||
</div>
|
||||
<div class="progress-bar" role="progressbar" v-bind:class="{
|
||||
'progress-bar-striped': state.charging,
|
||||
'progress-bar-animated': state.charging,
|
||||
'bg-warning': true,
|
||||
'bg-muted': true,
|
||||
}" v-bind:style="{width: minSoCRemainingDisplayWidth+'%'}"
|
||||
v-if="minSoCActive && (socChargeDisplayWidth < 100)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="config-template">
|
||||
<div class="container">
|
||||
<!-- <h1 class="display-4 pt-3 mx-auto text-center">Konfiguration</h1>
|
||||
<p class="lead mx-auto text-center">Details der Fahrzeug-, Wallbox- und Zählerkonfiguration.</p> -->
|
||||
|
||||
<div class="row mt-4 border-bottom">
|
||||
<div class="col-12">
|
||||
<p class="h1">{{state.title||"Home"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row h5">
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Netzzähler:
|
||||
<span class="text-primary" v-if="state.gridConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
PV Zähler:
|
||||
<span class="text-primary" v-if="state.pvConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Batteriezähler:
|
||||
<span class="text-primary" v-if="state.batteryConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="(loadpoint,id) in state.loadpoints" v-bind:loadpoint="loadpoint" :id="'loadpoint-'+id">
|
||||
<div class="row mt-4 border-bottom">
|
||||
<div class="col-12">
|
||||
<p class="h1">{{loadpoint.title||"Ladepunkt"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row h5">
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Ladezähler:
|
||||
<span class="text-primary" v-if="loadpoint.chargeConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Phasen:
|
||||
<span class="text-primary">{{loadpoint.phases}}p</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Min. Strom:
|
||||
<span class="text-primary">{{loadpoint.minCurrent}}A</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Max. Strom:
|
||||
<span class="text-primary">{{loadpoint.maxCurrent}}A</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row h5">
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-md-8 h2">
|
||||
<div class="row py-3 h2 border-bottom">
|
||||
<div class="col-12">
|
||||
Fahrzeug
|
||||
</div>
|
||||
<footer class="pt-4 pb-2 pt-md-5 pb-md-4 mt-3 mt-md-5 border-top">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<small class="text-muted">
|
||||
<a href="https://github.com/andig/evcc/releases/tag/[[.Version]]">[[.Version]]</a>
|
||||
(<a href="https://github.com/andig/evcc/commit/[[.Commit]]">[[.Commit]]</a>)
|
||||
</small>
|
||||
</div>
|
||||
<div class="row h5">
|
||||
<div class="col-6 py-3">
|
||||
Modell:
|
||||
<span class="text-primary">{{loadpoint.socTitle||"—"}}</span>
|
||||
</div>
|
||||
<div class="col-6 py-3">
|
||||
Kapazität:
|
||||
<span class="text-primary">{{loadpoint.socCapacity}}kWh</span>
|
||||
</div>
|
||||
<div class="col-6 text-right">
|
||||
<small class="text-muted">© 2020</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<footer class="pt-4 pb-2 pt-md-5 pb-md-4 mt-3 mt-md-5 border-top">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<small class="text-muted">
|
||||
<a href="https://github.com/andig/evcc/releases/tag/[[.Version]]">[[.Version]]</a>
|
||||
(<a href="https://github.com/andig/evcc/commit/[[.Commit]]">[[.Commit]]</a>)
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-6 text-right">
|
||||
<small class="text-muted">© 2020</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="js/app.js"></script>
|
||||
|
||||
</body>
|
||||
</footer>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
443
assets/js/app.js
443
assets/js/app.js
|
|
@ -1,227 +1,54 @@
|
|||
import $ from "jquery";
|
||||
import "popper.js"
|
||||
import "bootstrap"
|
||||
import Vue from "vue"
|
||||
import VueRouter from "vue-router"
|
||||
import axios from "axios"
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faSun, faArrowUp, faArrowDown, faTemperatureLow, faTemperatureHigh, faThermometerHalf, faLeaf, faChevronUp, faChevronDown, faExclamationTriangle }from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import "popper.js";
|
||||
import "bootstrap";
|
||||
import Vue from "vue";
|
||||
import axios from "axios";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
|
||||
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
|
||||
import { faArrowDown } from "@fortawesome/free-solid-svg-icons/faArrowDown";
|
||||
import { faTemperatureLow } from "@fortawesome/free-solid-svg-icons/faTemperatureLow";
|
||||
import { faTemperatureHigh } from "@fortawesome/free-solid-svg-icons/faTemperatureHigh";
|
||||
import { faThermometerHalf } from "@fortawesome/free-solid-svg-icons/faThermometerHalf";
|
||||
import { faLeaf } from "@fortawesome/free-solid-svg-icons/faLeaf";
|
||||
import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
|
||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
|
||||
import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons/faExclamationTriangle";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import App from "./views/App";
|
||||
import Toasts from "./components/Toasts";
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
|
||||
library.add(faSun, faArrowUp, faArrowDown, faTemperatureLow, faTemperatureHigh, faThermometerHalf, faLeaf, faChevronUp, faChevronDown, faExclamationTriangle)
|
||||
library.add(
|
||||
faSun,
|
||||
faArrowUp,
|
||||
faArrowDown,
|
||||
faTemperatureLow,
|
||||
faTemperatureHigh,
|
||||
faThermometerHalf,
|
||||
faLeaf,
|
||||
faChevronUp,
|
||||
faChevronDown,
|
||||
faExclamationTriangle
|
||||
);
|
||||
|
||||
Vue.component('fa-icon', FontAwesomeIcon)
|
||||
|
||||
Vue.use(VueRouter)
|
||||
Vue.component("font-awesome-icon", FontAwesomeIcon);
|
||||
|
||||
const loc = window.location;
|
||||
axios.defaults.baseURL = loc.protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") + loc.pathname + "api";
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/json';
|
||||
axios.defaults.baseURL =
|
||||
loc.protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") + loc.pathname + "api";
|
||||
axios.defaults.headers.post["Content-Type"] = "application/json";
|
||||
|
||||
//
|
||||
// Mixins
|
||||
//
|
||||
|
||||
let formatter = {
|
||||
data: function () {
|
||||
return {
|
||||
fmtLimit: 100,
|
||||
fmtDigits: 1,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
round: function(num, precision) {
|
||||
var base = 10 ** precision;
|
||||
return (Math.round(num * base) / base).toFixed(precision);
|
||||
},
|
||||
fmt: function (val) {
|
||||
if (val === undefined || val === null) {
|
||||
return 0;
|
||||
}
|
||||
val = Math.abs(val);
|
||||
return val >= this.fmtLimit ? this.round(val / 1e3, this.fmtDigits) : this.round(val, 0);
|
||||
},
|
||||
fmtUnit: function (val) {
|
||||
return Math.abs(val) >= this.fmtLimit ? "k" : "";
|
||||
},
|
||||
fmtDuration: function (d) {
|
||||
if (d <= 0 || d == null) {
|
||||
return '—';
|
||||
}
|
||||
var seconds = "0" + (d % 60);
|
||||
var minutes = "0" + (Math.floor(d / 60) % 60);
|
||||
var hours = "" + Math.floor(d / 3600);
|
||||
if (hours.length < 2) {
|
||||
hours = "0" + hours;
|
||||
}
|
||||
return hours + ":" + minutes.substr(-2) + ":" + seconds.substr(-2);
|
||||
},
|
||||
fmtShortDuration: function (d) {
|
||||
if (d <= 0 || d == null) {
|
||||
return '—';
|
||||
}
|
||||
var minutes = (Math.floor(d / 60) % 60);
|
||||
var hours = Math.floor(d / 3600);
|
||||
var tm;
|
||||
if (hours >= 1) {
|
||||
minutes = "0" + minutes;
|
||||
tm = hours + ":" + minutes.substr(-2);
|
||||
} else {
|
||||
var seconds = "0" + (d % 60);
|
||||
tm = minutes + ":" + seconds.substr(-2);
|
||||
}
|
||||
return tm;
|
||||
},
|
||||
fmtShortDurationUnit: function (d) {
|
||||
if (d <= 0 || d == null) {
|
||||
return '';
|
||||
}
|
||||
var hours = Math.floor(d / 3600);
|
||||
if (hours >= 1) {
|
||||
return "h";
|
||||
}
|
||||
return "m";
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// State
|
||||
//
|
||||
|
||||
function setProperty(obj, props, value) {
|
||||
const prop = props.shift()
|
||||
if (!obj[prop]) {
|
||||
Vue.set(obj, prop, {})
|
||||
}
|
||||
|
||||
if (!props.length) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
obj[prop] = { ...obj[prop], ...value }
|
||||
} else {
|
||||
obj[prop] = value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setProperty(obj[prop], props, value)
|
||||
}
|
||||
|
||||
let store = {
|
||||
state: {
|
||||
loadpoints: [], // ensure array type
|
||||
},
|
||||
update: function(msg) {
|
||||
Object.keys(msg).forEach(function (k) {
|
||||
if (typeof toasts[k] === "function") {
|
||||
toasts[k]({message: msg[k]})
|
||||
} else {
|
||||
setProperty(store.state, k.split('.'), msg[k])
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Heartbeat
|
||||
//
|
||||
|
||||
window.setInterval(function() {
|
||||
axios.get("health").catch(function(res) {
|
||||
res.message = "Server unavailable";
|
||||
toasts.error(res)
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
//
|
||||
// App & Routing
|
||||
//
|
||||
|
||||
const main = Vue.component('Main', {
|
||||
template: '#main-template',
|
||||
data: function() {
|
||||
return {
|
||||
state: store.state // global state
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
configured: function (val) {
|
||||
// for development purposes
|
||||
if (val == '[[.Configured]]') {
|
||||
return true;
|
||||
}
|
||||
if (!isNaN(parseInt(val)) && parseInt(val) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const config = Vue.component("config", {
|
||||
template: "#config-template",
|
||||
data: function() {
|
||||
return {
|
||||
state: store.state // global state
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const routes = [
|
||||
{ path: "/", component: main },
|
||||
{ path: "/config", component: config },
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
routes, // short for `routes: routes`
|
||||
linkExactActiveClass: "active", // Bootstrap <nav>
|
||||
});
|
||||
|
||||
const app = new Vue({
|
||||
el: "#app",
|
||||
router: router,
|
||||
data: {
|
||||
compact: false,
|
||||
},
|
||||
methods: {
|
||||
connect: function() {
|
||||
const protocol = loc.protocol == "https:" ? "wss:" : "ws:";
|
||||
const uri = protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") + loc.pathname + "ws";
|
||||
const ws = new WebSocket(uri), self = this;
|
||||
ws.onerror = function(evt) {
|
||||
ws.close();
|
||||
};
|
||||
ws.onclose = function(evt) {
|
||||
window.setTimeout(self.connect, 1000);
|
||||
};
|
||||
ws.onmessage = function(evt) {
|
||||
try {
|
||||
var msg = JSON.parse(evt.data);
|
||||
store.update(msg);
|
||||
}
|
||||
catch (e) {
|
||||
toasts.error(e, evt.data)
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.compact = urlParams.get("compact");
|
||||
this.connect(); // websocket listener
|
||||
},
|
||||
});
|
||||
|
||||
//
|
||||
// Components
|
||||
//
|
||||
|
||||
const toasts = new Vue({
|
||||
window.toasts = new Vue({
|
||||
el: "#toasts",
|
||||
render: function (h) {
|
||||
return h(Toasts, { props: { items: this.items, count: this.count } });
|
||||
},
|
||||
data: {
|
||||
items: {},
|
||||
count: 0,
|
||||
},
|
||||
name: "ToastsRoot",
|
||||
methods: {
|
||||
raise: function (msg) {
|
||||
let found = false;
|
||||
|
|
@ -238,7 +65,7 @@ const toasts = new Vue({
|
|||
},
|
||||
error: function (msg) {
|
||||
msg.type = "error";
|
||||
this.raise(msg)
|
||||
this.raise(msg);
|
||||
},
|
||||
warn: function (msg) {
|
||||
msg.type = "warn";
|
||||
|
|
@ -247,183 +74,19 @@ const toasts = new Vue({
|
|||
remove: function (msg) {
|
||||
Vue.delete(this.items, msg.id);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('message-toast', {
|
||||
template: '#message-template',
|
||||
props: ['item'],
|
||||
mounted: function () {
|
||||
const id = "#message-id-" + this.item.id;
|
||||
$(id).toast('show');
|
||||
$(id).on('hidden.bs.toast', function () {
|
||||
toasts.remove(this.item);
|
||||
}.bind(this))
|
||||
},
|
||||
});
|
||||
|
||||
Vue.component('version', {
|
||||
template: '#version-template',
|
||||
props: ['installed'],
|
||||
data: function () {
|
||||
return {
|
||||
state: store.state,
|
||||
notesShown: false,
|
||||
};
|
||||
},
|
||||
mounted: function () {
|
||||
$(this.$refs.notes)
|
||||
.on('show.bs.collapse', function () { this.notesShown = true; }.bind(this))
|
||||
.on('hide.bs.collapse', function () { this.notesShown = false; }.bind(this));
|
||||
},
|
||||
watch: {
|
||||
"state.availableVersion": function () {
|
||||
if (this.installed != "[[.Version]]" && // go template parsed?
|
||||
this.installed != "0.0.1-alpha" && // make used?
|
||||
this.state.availableVersion != this.installed) {
|
||||
$(this.$refs.bar).collapse("show");
|
||||
}
|
||||
}
|
||||
}
|
||||
new Vue({
|
||||
el: "#app",
|
||||
router,
|
||||
data: { store },
|
||||
render: (h) => h(App),
|
||||
});
|
||||
|
||||
Vue.component('site', {
|
||||
template: '#site-template',
|
||||
props: ['state'],
|
||||
mixins: [formatter],
|
||||
computed: {
|
||||
multi: function() {
|
||||
return this.state.loadpoints.length > 1 || app.compact;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('site-details', {
|
||||
template: '#site-details-template',
|
||||
props: ['state'],
|
||||
mixins: [formatter]
|
||||
});
|
||||
|
||||
Vue.component("loadpoint", {
|
||||
template: "#loadpoint-template",
|
||||
props: ["state", "id", "pv", "multi"],
|
||||
mixins: [formatter],
|
||||
data: function() {
|
||||
return {
|
||||
tickerHandle: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasTargetSoC: function () {
|
||||
return this.state.socLevels != null && this.state.socLevels.length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"state.chargeDuration": function() {
|
||||
window.clearInterval(this.tickerHandle);
|
||||
// only ticker if actually charging
|
||||
if (this.state.charging && this.state.chargeDuration >= 0) {
|
||||
this.tickerHandle = window.setInterval(function() {
|
||||
this.state.chargeDuration += 1;
|
||||
}.bind(this), 1000);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
api: function (func) {
|
||||
return "loadpoints/" + this.id + "/" + func;
|
||||
},
|
||||
targetMode: function (mode) {
|
||||
axios.post(this.api("mode") + "/" + mode).then(function (response) {
|
||||
this.state.mode = response.data.mode;
|
||||
}.bind(this)).catch(toasts.error);
|
||||
},
|
||||
targetSoC: function (soc) {
|
||||
axios.post(this.api("targetsoc") + "/" + soc).then(function (response) {
|
||||
this.state.targetSoC = response.data.targetSoC;
|
||||
}.bind(this)).catch(toasts.error);
|
||||
},
|
||||
},
|
||||
destroyed: function() {
|
||||
window.clearInterval(this.tickerHandle);
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component("loadpoint-details", {
|
||||
template: "#loadpoint-details-template",
|
||||
props: ["state"],
|
||||
mixins: [formatter],
|
||||
computed: {
|
||||
minSoCActive: function () {
|
||||
return this.state.connected && (
|
||||
this.state.minSoC > 0 && this.state.socCharge < this.state.minSoC
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component("vehicle", {
|
||||
template: "#vehicle-template",
|
||||
props: ["state"],
|
||||
computed: {
|
||||
socChargeDisplayWidth: function () {
|
||||
if (this.state.soc && this.state.socCharge >= 0) {
|
||||
return this.state.socCharge;
|
||||
}
|
||||
return 100;
|
||||
},
|
||||
socChargeDisplayValue: function () {
|
||||
// no soc or no soc value
|
||||
if (!this.state.soc || this.state.socCharge < 0) {
|
||||
let chargeStatus = "getrennt";
|
||||
if (this.state.charging) {
|
||||
chargeStatus = "laden";
|
||||
} else if (this.state.connected) {
|
||||
chargeStatus = "verbunden";
|
||||
}
|
||||
return chargeStatus;
|
||||
}
|
||||
|
||||
// percent value if enough space
|
||||
let socCharge = this.state.socCharge;
|
||||
if (socCharge >= 10) {
|
||||
socCharge += "%";
|
||||
}
|
||||
return socCharge;
|
||||
},
|
||||
minSoCActive: function () {
|
||||
return this.state.minSoC > 0 && this.state.socCharge < this.state.minSoC;
|
||||
},
|
||||
minSoCRemainingDisplayWidth: function () {
|
||||
return this.state.minSoC - this.state.socCharge;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component("mode", {
|
||||
template: "#mode-template",
|
||||
props: ["mode", "pv", "caption"],
|
||||
methods: {
|
||||
targetMode: function (mode) {
|
||||
this.$emit("updated", mode)
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Vue.component("soc", {
|
||||
template: "#soc-template",
|
||||
props: ["soc", "caption", "levels"],
|
||||
computed: {
|
||||
levelsOrDefault: function() {
|
||||
if (this.levels == null || this.levels.length == 0) {
|
||||
return []; // disabled, or use 30, 50, 80, 100
|
||||
}
|
||||
return this.levels;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
targetSoC: function (mode) {
|
||||
this.$emit("updated", mode)
|
||||
}
|
||||
},
|
||||
});
|
||||
window.setInterval(function () {
|
||||
axios.get("health").catch(function (res) {
|
||||
res.message = "Server unavailable";
|
||||
window.toasts.error(res);
|
||||
});
|
||||
}, 5000);
|
||||
|
|
|
|||
200
assets/js/components/Loadpoint.vue
Normal file
200
assets/js/components/Loadpoint.vue
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="row" v-if="multi">
|
||||
<div class="col-12 col-md-4 d-md-flex mt-3 mt-md-5 align-items-end">
|
||||
<span class="h1 align-bottom">{{ state.title || "Ladepunkt" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-8 d-none d-md-block mt-3 mt-md-5">
|
||||
<LoadpointDetails v-bind:state="state"></LoadpointDetails>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-md-none">
|
||||
<div class="row mt-3 pb-3 bg-light">
|
||||
<div class="col-12 mt-3">
|
||||
<Mode
|
||||
class="w-100"
|
||||
v-bind:mode="state.mode"
|
||||
:pv="pv"
|
||||
v-on:updated="targetMode"
|
||||
></Mode>
|
||||
</div>
|
||||
<div class="col-12 mt-3" v-if="hasTargetSoC">
|
||||
<Soc
|
||||
class="w-100"
|
||||
v-bind:soc="state.targetSoC"
|
||||
:levels="state.socLevels"
|
||||
v-on:updated="targetSoC"
|
||||
></Soc>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-none d-md-flex mt-5 py-3 pb-4 text-center bg-light" v-if="!multi">
|
||||
<div
|
||||
class="mt-3"
|
||||
v-bind:class="{ 'col-md-6': hasTargetSoC, 'col-md-12': !hasTargetSoC }"
|
||||
>
|
||||
<Mode
|
||||
v-bind:mode="state.mode"
|
||||
:pv="pv"
|
||||
:caption="true"
|
||||
v-on:updated="targetMode"
|
||||
></Mode>
|
||||
</div>
|
||||
<div class="col-md-6 mt-3" v-if="hasTargetSoC">
|
||||
<Soc
|
||||
v-bind:soc="state.targetSoC"
|
||||
:levels="state.socLevels"
|
||||
:caption="true"
|
||||
v-on:updated="targetSoC"
|
||||
></Soc>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-md-none mt-2 pb-3 bg-light" v-if="!multi">
|
||||
<div class="col-12 mt-3">
|
||||
<Mode
|
||||
class="w-100"
|
||||
v-bind:mode="state.mode"
|
||||
:pv="pv"
|
||||
v-on:updated="targetMode"
|
||||
></Mode>
|
||||
</div>
|
||||
<div class="col-12 mt-3" v-if="hasTargetSoC">
|
||||
<Soc
|
||||
class="w-100"
|
||||
v-bind:soc="state.targetSoC"
|
||||
:levels="state.socLevels"
|
||||
v-on:updated="targetSoC"
|
||||
></Soc>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="!multi">
|
||||
<div class="col-12 col-md-4 d-none d-md-flex mt-3 mt-md-5">
|
||||
<span class="h1">{{ state.title || "Ladepunkt" }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="col-12 col-md-8 d-flex d-md-flex mt-3 mt-md-5 pt-3"
|
||||
v-if="state.remoteDisabled"
|
||||
>
|
||||
<h5 class="w-100">
|
||||
<span class="badge badge-warning w-100" v-if="state.remoteDisabled == 'soft'">
|
||||
{{ state.remoteDisabledSource }}: Adaptives PV-Laden deaktiviert
|
||||
</span>
|
||||
<span class="badge badge-danger w-100" v-if="state.remoteDisabled == 'hard'">
|
||||
{{ state.remoteDisabledSource }}: Deaktiviert
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row border-bottom d-none d-md-block"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 mt-3 mb-3 mb-md-0">
|
||||
<Vehicle v-bind:state="state"></Vehicle>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 d-none d-md-block mt-3" v-if="multi">
|
||||
<div class="mb-2">Modus</div>
|
||||
<Mode
|
||||
class="btn-group-sm"
|
||||
v-bind:mode="state.mode"
|
||||
:pv="pv"
|
||||
v-on:updated="targetMode"
|
||||
></Mode>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 d-none d-md-block mt-3" v-if="multi && hasTargetSoC">
|
||||
<div class="mb-2">Ladeziel</div>
|
||||
<Soc
|
||||
class="btn-group-sm"
|
||||
v-bind:soc="state.targetSoC"
|
||||
:levels="state.socLevels"
|
||||
v-on:updated="targetSoC"
|
||||
></Soc>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 d-none d-md-block" v-if="!multi">
|
||||
<LoadpointDetails v-bind:state="state"></LoadpointDetails>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-md-none">
|
||||
<LoadpointDetails v-bind:state="state"></LoadpointDetails>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Soc from "./Soc";
|
||||
import Mode from "./Mode";
|
||||
import Vehicle from "./Vehicle";
|
||||
import LoadpointDetails from "./LoadpointDetails";
|
||||
import formatter from "../mixins/formatter";
|
||||
|
||||
export default {
|
||||
name: "Loadpoint",
|
||||
props: ["state", "id", "pv", "multi"],
|
||||
components: { LoadpointDetails, Soc, Mode, Vehicle },
|
||||
mixins: [formatter],
|
||||
data: function () {
|
||||
return {
|
||||
tickerHandle: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasTargetSoC: function () {
|
||||
return this.state.socLevels != null && this.state.socLevels.length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"state.chargeDuration": function () {
|
||||
window.clearInterval(this.tickerHandle);
|
||||
// only ticker if actually charging
|
||||
if (this.state.charging && this.state.chargeDuration >= 0) {
|
||||
this.tickerHandle = window.setInterval(
|
||||
function () {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.state.chargeDuration += 1;
|
||||
}.bind(this),
|
||||
1000
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
api: function (func) {
|
||||
return "loadpoints/" + this.id + "/" + func;
|
||||
},
|
||||
targetMode: function (mode) {
|
||||
axios
|
||||
.post(this.api("mode") + "/" + mode)
|
||||
.then(
|
||||
function (response) {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.state.mode = response.data.mode;
|
||||
}.bind(this)
|
||||
)
|
||||
.catch(window.toasts.error);
|
||||
},
|
||||
targetSoC: function (soc) {
|
||||
axios
|
||||
.post(this.api("targetsoc") + "/" + soc)
|
||||
.then(
|
||||
function (response) {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.state.targetSoC = response.data.targetSoC;
|
||||
}.bind(this)
|
||||
)
|
||||
.catch(window.toasts.error);
|
||||
},
|
||||
},
|
||||
destroyed: function () {
|
||||
window.clearInterval(this.tickerHandle);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
70
assets/js/components/LoadpointDetails.vue
Normal file
70
assets/js/components/LoadpointDetails.vue
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-3 mt-3">
|
||||
<div class="mb-2 value">
|
||||
Leistung
|
||||
<font-awesome-icon
|
||||
class="text-primary ml-1"
|
||||
icon="temperature-low"
|
||||
v-if="state.climater == 'heating'"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
class="text-primary ml-1"
|
||||
icon="temperature-high"
|
||||
v-if="state.climater == 'cooling'"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
class="text-primary ml-1"
|
||||
icon="thermometer-half"
|
||||
v-if="state.climater == 'on'"
|
||||
/>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
{{ fmt(state.chargePower) }}
|
||||
<small class="text-muted">{{ fmtUnit(state.chargePower) }}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 mt-3">
|
||||
<div class="mb-2 value">Geladen</div>
|
||||
<h2 class="value">
|
||||
{{ fmt(state.chargedEnergy) }}
|
||||
<small class="text-muted">{{ fmtUnit(state.chargedEnergy) }}Wh</small>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3 mt-3">
|
||||
<div class="mb-2 value">Dauer</div>
|
||||
<h2 class="value">
|
||||
{{ fmtShortDuration(state.chargeDuration) }}
|
||||
<small class="text-muted">{{ fmtShortDurationUnit(state.chargeDuration) }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.soc">
|
||||
<div class="mb-2 value">Restzeit</div>
|
||||
<h2 class="value">
|
||||
{{ fmtShortDuration(state.chargeEstimate) }}
|
||||
<small class="text-muted">{{ fmtShortDurationUnit(state.chargeEstimate) }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import formatter from "../mixins/formatter";
|
||||
|
||||
export default {
|
||||
name: "LoadpointDetails",
|
||||
props: ["state"],
|
||||
mixins: [formatter],
|
||||
computed: {
|
||||
minSoCActive: function () {
|
||||
return (
|
||||
this.state.connected &&
|
||||
this.state.minSoC > 0 &&
|
||||
this.state.socCharge < this.state.minSoC
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
37
assets/js/components/MessageToast.vue
Normal file
37
assets/js/components/MessageToast.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div class="toast" data-delay="10000" v-bind:data-autohide="true">
|
||||
<div class="toast-header">
|
||||
<strong class="mr-auto" v-if="item.type != 'warn'"
|
||||
><font-awesome-icon class="text-danger" icon="exclamation-triangle" /> Error</strong
|
||||
>
|
||||
<strong class="mr-auto" v-if="item.type == 'warn'"
|
||||
><font-awesome-icon class="text-warning" icon="exclamation-triangle" />
|
||||
Warning</strong
|
||||
>
|
||||
<small v-if="item.status">HTTP {{ item.status }}</small>
|
||||
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="toast-body">{{ item.message }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from "jquery";
|
||||
|
||||
export default {
|
||||
name: "MessageToast",
|
||||
props: ["item"],
|
||||
mounted: function () {
|
||||
const id = "#message-id-" + this.item.id;
|
||||
$(id).toast("show");
|
||||
$(id).on(
|
||||
"hidden.bs.toast",
|
||||
function () {
|
||||
window.toasts.remove(this.item);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
41
assets/js/components/Mode.vue
Normal file
41
assets/js/components/Mode.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div class="btn-group btn-group-toggle bg-white shadow-none">
|
||||
<label
|
||||
class="btn btn-outline-primary value disabled caption font-weight-bold"
|
||||
v-if="caption"
|
||||
>
|
||||
Modus
|
||||
</label>
|
||||
<label
|
||||
class="btn btn-outline-primary"
|
||||
v-bind:class="{ active: mode == 'off', first: !caption }"
|
||||
>
|
||||
<input type="radio" value="off" v-on:click="targetMode('off')" />Stop
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-bind:class="{ active: mode == 'now' }">
|
||||
<input type="radio" value="now" v-on:click="targetMode('now')" />Sofort
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-bind:class="{ active: mode == 'minpv' }" v-if="pv">
|
||||
<input type="radio" value="minpv" v-on:click="targetMode('minpv')" />
|
||||
<span class="d-inline d-lg-none">Min</span>
|
||||
<span class="d-none d-lg-inline">Min + PV</span>
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-bind:class="{ active: mode == 'pv' }" v-if="pv">
|
||||
<input type="radio" value="pv" v-on:click="targetMode('pv')" />
|
||||
<span class="d-inline d-md-none">PV</span>
|
||||
<span class="d-none d-md-inline">Nur PV</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Mode",
|
||||
props: ["mode", "pv", "caption"],
|
||||
methods: {
|
||||
targetMode: function (mode) {
|
||||
this.$emit("updated", mode);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
50
assets/js/components/Site.vue
Normal file
50
assets/js/components/Site.vue
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="d-none d-md-flex col-12 col-md-4 mt-md-4 align-items-end">
|
||||
<p class="h1">{{ state.title || "Home" }}</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 mt-md-4" v-if="multi">
|
||||
<SiteDetails v-bind:state="state"></SiteDetails>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-none d-md-flex border-bottom"></div>
|
||||
|
||||
<div class="row" v-if="!multi">
|
||||
<div class="d-none d-md-block col-md-4"></div>
|
||||
<div class="col-12 col-md-8">
|
||||
<SiteDetails v-bind:state="state"></SiteDetails>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Loadpoint
|
||||
v-for="(loadpoint, id) in state.loadpoints"
|
||||
v-bind:id="id"
|
||||
:key="id"
|
||||
:state="loadpoint"
|
||||
:pv="state.gridConfigured"
|
||||
:multi="multi"
|
||||
>
|
||||
</Loadpoint>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SiteDetails from "./SiteDetails";
|
||||
import Loadpoint from "./Loadpoint";
|
||||
import formatter from "../mixins/formatter";
|
||||
|
||||
export default {
|
||||
name: "Site",
|
||||
props: ["state"],
|
||||
components: { SiteDetails, Loadpoint },
|
||||
mixins: [formatter],
|
||||
computed: {
|
||||
multi: function () {
|
||||
// TODO fix compact
|
||||
return this.state.loadpoints.length > 1 /* || app.compact*/;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
79
assets/js/components/SiteDetails.vue
Normal file
79
assets/js/components/SiteDetails.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.gridConfigured">
|
||||
<div class="mb-2 value" v-if="state.gridPower > 0">
|
||||
Bezug <font-awesome-icon icon="arrow-down" class="text-primary" />
|
||||
</div>
|
||||
<div class="mb-2 value" v-else>
|
||||
Einspeisung <font-awesome-icon icon="arrow-up" class="text-primary" />
|
||||
</div>
|
||||
<h2 class="value">
|
||||
{{ fmt(state.gridPower) }}
|
||||
<small class="text-muted">{{ fmtUnit(state.gridPower) }}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.pvConfigured">
|
||||
<div class="mb-2 value">
|
||||
Erzeugung
|
||||
<font-awesome-icon
|
||||
icon="sun"
|
||||
v-bind:class="{
|
||||
'text-primary': state.pvPower < 0,
|
||||
'text-muted': state.pvPower >= 0,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
{{ fmt(state.pvPower) }}
|
||||
<small class="text-muted">{{ fmtUnit(state.pvPower) }}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="d-md-block col-6 col-md-3 mt-3"
|
||||
v-bind:class="{ 'd-none': !state.batterySoC }"
|
||||
v-if="state.batteryConfigured"
|
||||
>
|
||||
<div class="mb-2 value">
|
||||
Batterie
|
||||
<font-awesome-icon
|
||||
class="text-primary"
|
||||
icon="arrow-down"
|
||||
v-if="state.batteryPower < 0"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
class="text-primary"
|
||||
icon="arrow-up"
|
||||
v-if="state.batteryPower > 0"
|
||||
/>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
{{ fmt(state.batteryPower) }}
|
||||
<small class="text-muted">{{ fmtUnit(state.batteryPower) }}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.batterySoC">
|
||||
<div class="mb-2 value">
|
||||
SoC
|
||||
<font-awesome-icon
|
||||
class="text-primary"
|
||||
icon="battery-three-quarters"
|
||||
v-bind:class="{
|
||||
'text-primary': state.batteryPower > 0,
|
||||
'text-muted': state.batteryPower < 0,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<h2 class="value">{{ state.batterySoC }} <small class="text-muted">%</small></h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import formatter from "../mixins/formatter";
|
||||
|
||||
export default {
|
||||
name: "SiteDetails",
|
||||
props: ["state"],
|
||||
mixins: [formatter],
|
||||
};
|
||||
</script>
|
||||
37
assets/js/components/Soc.vue
Normal file
37
assets/js/components/Soc.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div class="btn-group btn-group-toggle bg-white shadow-none">
|
||||
<label class="btn btn-outline-primary disabled caption font-weight-bold" v-if="caption">
|
||||
Ladeziel
|
||||
</label>
|
||||
<label
|
||||
class="btn btn-outline-primary"
|
||||
v-for="(level, id) in levelsOrDefault"
|
||||
:key="id"
|
||||
v-bind:level="level"
|
||||
:id="id"
|
||||
:class="{ active: soc == level, first: !caption && id == 0 }"
|
||||
>
|
||||
<input type="radio" v-bind:value="level" v-on:click="targetSoC(level)" />{{ level }}%
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Soc",
|
||||
props: ["soc", "caption", "levels"],
|
||||
computed: {
|
||||
levelsOrDefault: function () {
|
||||
if (this.levels == null || this.levels.length == 0) {
|
||||
return []; // disabled, or use 30, 50, 80, 100
|
||||
}
|
||||
return this.levels;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
targetSoC: function (mode) {
|
||||
this.$emit("updated", mode);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
22
assets/js/components/Toasts.vue
Normal file
22
assets/js/components/Toasts.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<div aria-atomic="true" style="position: absolute; top: 4rem; right: 0.5rem">
|
||||
<!-- Position it -->
|
||||
<MessageToast
|
||||
v-for="item in items"
|
||||
v-bind:item="item"
|
||||
:id="'message-id-' + item.id"
|
||||
:key="item.id"
|
||||
>
|
||||
</MessageToast>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MessageToast from "./MessageToast";
|
||||
|
||||
export default {
|
||||
name: "Toasts",
|
||||
components: { MessageToast },
|
||||
props: ["items", "count"],
|
||||
};
|
||||
</script>
|
||||
73
assets/js/components/Vehicle.vue
Normal file
73
assets/js/components/Vehicle.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="mb-2">{{ state.socTitle || "Fahrzeug" }}</div>
|
||||
<div class="progress" style="height: 24px; font-size: 100%; margin-top: 16px">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
v-bind:class="{
|
||||
'progress-bar-striped': state.charging,
|
||||
'progress-bar-animated': state.charging,
|
||||
'bg-light': !state.connected,
|
||||
'text-secondary': !state.connected,
|
||||
'bg-warning': state.connected && minSoCActive,
|
||||
}"
|
||||
v-bind:style="{ width: socChargeDisplayWidth + '%' }"
|
||||
>
|
||||
{{ socChargeDisplayValue }}
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
v-bind:class="{
|
||||
'progress-bar-striped': state.charging,
|
||||
'progress-bar-animated': state.charging,
|
||||
'bg-warning': true,
|
||||
'bg-muted': true,
|
||||
}"
|
||||
v-bind:style="{ width: minSoCRemainingDisplayWidth + '%' }"
|
||||
v-if="minSoCActive && socChargeDisplayWidth < 100"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Vehicle",
|
||||
props: ["state"],
|
||||
computed: {
|
||||
socChargeDisplayWidth: function () {
|
||||
if (this.state.soc && this.state.socCharge >= 0) {
|
||||
return this.state.socCharge;
|
||||
}
|
||||
return 100;
|
||||
},
|
||||
socChargeDisplayValue: function () {
|
||||
// no soc or no soc value
|
||||
if (!this.state.soc || this.state.socCharge < 0) {
|
||||
let chargeStatus = "getrennt";
|
||||
if (this.state.charging) {
|
||||
chargeStatus = "laden";
|
||||
} else if (this.state.connected) {
|
||||
chargeStatus = "verbunden";
|
||||
}
|
||||
return chargeStatus;
|
||||
}
|
||||
|
||||
// percent value if enough space
|
||||
let socCharge = this.state.socCharge;
|
||||
if (socCharge >= 10) {
|
||||
socCharge += "%";
|
||||
}
|
||||
return socCharge;
|
||||
},
|
||||
minSoCActive: function () {
|
||||
return this.state.minSoC > 0 && this.state.socCharge < this.state.minSoC;
|
||||
},
|
||||
minSoCRemainingDisplayWidth: function () {
|
||||
return this.state.minSoC - this.state.socCharge;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
86
assets/js/components/Version.vue
Normal file
86
assets/js/components/Version.vue
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div class="collapse" ref="bar">
|
||||
<div class="row p-3 bg-warning">
|
||||
<div class="col-12">
|
||||
Neue Version verfügbar! Installiert: {{ installed }}. Verfügbar:
|
||||
{{ state.availableVersion }}.
|
||||
<b
|
||||
class="px-3"
|
||||
data-toggle="collapse"
|
||||
data-target="#release-notes"
|
||||
v-if="state.releaseNotes"
|
||||
>
|
||||
<a href="#" class="text-body">
|
||||
Release notes
|
||||
<font-awesome-icon icon="chevron-up" v-if="notesShown" />
|
||||
<font-awesome-icon icon="chevron-down" v-if="!notesShown" />
|
||||
</a>
|
||||
</b>
|
||||
<b class="px-3">
|
||||
<a
|
||||
v-bind:href="
|
||||
'https://github.com/andig/evcc/releases/tag/' + state.availableVersion
|
||||
"
|
||||
class="text-body"
|
||||
>
|
||||
Download <font-awesome-icon icon="chevron-down" />
|
||||
</a>
|
||||
</b>
|
||||
<button
|
||||
type="button"
|
||||
class="close float-right"
|
||||
style="margin-top: -2px"
|
||||
aria-label="Close"
|
||||
data-toggle="collapse"
|
||||
data-target="#version-bar"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row p-3 bg-light collapse" id="release-notes" ref="notes">
|
||||
<div class="col-12" v-html="state.releaseNotes"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from "jquery";
|
||||
|
||||
export default {
|
||||
name: "Version",
|
||||
props: ["installed"],
|
||||
data: function () {
|
||||
return {
|
||||
state: this.$root.$data.store.state,
|
||||
notesShown: false,
|
||||
};
|
||||
},
|
||||
mounted: function () {
|
||||
$(this.$refs.notes)
|
||||
.on(
|
||||
"show.bs.collapse",
|
||||
function () {
|
||||
this.notesShown = true;
|
||||
}.bind(this)
|
||||
)
|
||||
.on(
|
||||
"hide.bs.collapse",
|
||||
function () {
|
||||
this.notesShown = false;
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
watch: {
|
||||
"state.availableVersion": function () {
|
||||
if (
|
||||
this.installed != window.evcc.version && // go template parsed?
|
||||
this.installed != "0.0.1-alpha" && // make used?
|
||||
this.state.availableVersion != this.installed
|
||||
) {
|
||||
$(this.$refs.bar).collapse("show");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
62
assets/js/mixins/formatter.js
Normal file
62
assets/js/mixins/formatter.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
fmtLimit: 100,
|
||||
fmtDigits: 1,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
round: function (num, precision) {
|
||||
var base = 10 ** precision;
|
||||
return (Math.round(num * base) / base).toFixed(precision);
|
||||
},
|
||||
fmt: function (val) {
|
||||
if (val === undefined || val === null) {
|
||||
return 0;
|
||||
}
|
||||
val = Math.abs(val);
|
||||
return val >= this.fmtLimit ? this.round(val / 1e3, this.fmtDigits) : this.round(val, 0);
|
||||
},
|
||||
fmtUnit: function (val) {
|
||||
return Math.abs(val) >= this.fmtLimit ? "k" : "";
|
||||
},
|
||||
fmtDuration: function (d) {
|
||||
if (d <= 0 || d == null) {
|
||||
return "—";
|
||||
}
|
||||
var seconds = "0" + (d % 60);
|
||||
var minutes = "0" + (Math.floor(d / 60) % 60);
|
||||
var hours = "" + Math.floor(d / 3600);
|
||||
if (hours.length < 2) {
|
||||
hours = "0" + hours;
|
||||
}
|
||||
return hours + ":" + minutes.substr(-2) + ":" + seconds.substr(-2);
|
||||
},
|
||||
fmtShortDuration: function (d) {
|
||||
if (d <= 0 || d == null) {
|
||||
return "—";
|
||||
}
|
||||
var minutes = Math.floor(d / 60) % 60;
|
||||
var hours = Math.floor(d / 3600);
|
||||
var tm;
|
||||
if (hours >= 1) {
|
||||
minutes = "0" + minutes;
|
||||
tm = hours + ":" + minutes.substr(-2);
|
||||
} else {
|
||||
var seconds = "0" + (d % 60);
|
||||
tm = minutes + ":" + seconds.substr(-2);
|
||||
}
|
||||
return tm;
|
||||
},
|
||||
fmtShortDurationUnit: function (d) {
|
||||
if (d <= 0 || d == null) {
|
||||
return "";
|
||||
}
|
||||
var hours = Math.floor(d / 3600);
|
||||
if (hours >= 1) {
|
||||
return "h";
|
||||
}
|
||||
return "m";
|
||||
},
|
||||
},
|
||||
};
|
||||
16
assets/js/router.js
Normal file
16
assets/js/router.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import VueRouter from "vue-router";
|
||||
import Vue from "vue";
|
||||
import Main from "./views/Main";
|
||||
import Config from "./views/Config";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const routes = [
|
||||
{ path: "/", component: Main },
|
||||
{ path: "/config", component: Config },
|
||||
];
|
||||
|
||||
module.exports = new VueRouter({
|
||||
routes,
|
||||
linkExactActiveClass: "active", // Bootstrap <nav>
|
||||
});
|
||||
36
assets/js/store.js
Normal file
36
assets/js/store.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Vue from "vue";
|
||||
|
||||
function setProperty(obj, props, value) {
|
||||
const prop = props.shift();
|
||||
if (!obj[prop]) {
|
||||
Vue.set(obj, prop, {});
|
||||
}
|
||||
|
||||
if (!props.length) {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
obj[prop] = { ...obj[prop], ...value };
|
||||
} else {
|
||||
obj[prop] = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setProperty(obj[prop], props, value);
|
||||
}
|
||||
|
||||
const store = {
|
||||
state: {
|
||||
loadpoints: [], // ensure array type
|
||||
},
|
||||
update: function (msg) {
|
||||
Object.keys(msg).forEach(function (k) {
|
||||
if (typeof window.toasts[k] === "function") {
|
||||
window.toasts[k]({ message: msg[k] });
|
||||
} else {
|
||||
setProperty(store.state, k.split("."), msg[k]);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = store;
|
||||
89
assets/js/views/App.vue
Normal file
89
assets/js/views/App.vue
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<div>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="https://github.com/andig/evcc"
|
||||
><font-awesome-icon icon="leaf" class="text-primary mr-2" />evcc</a
|
||||
>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbarNavAltMarkup"
|
||||
aria-controls="navbarNavAltMarkup"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
|
||||
<div class="navbar-nav">
|
||||
<router-link class="nav-item nav-link pb-1" to="/">Laden</router-link>
|
||||
<router-link class="nav-item nav-link pb-1" to="/config"
|
||||
>Konfiguration</router-link
|
||||
>
|
||||
<a
|
||||
class="nav-item nav-link pb-1"
|
||||
href="https://github.com/andig/evcc/issues"
|
||||
target="_blank"
|
||||
>Support</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<Version :installed="installed" id="version-bar"></Version>
|
||||
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Version from "../components/Version";
|
||||
import store from "../store";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: { Version },
|
||||
data: function () {
|
||||
return {
|
||||
compact: false,
|
||||
store: this.$root.$data.store,
|
||||
installed: window.evcc.version,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
connect: function () {
|
||||
const loc = window.location;
|
||||
const protocol = loc.protocol == "https:" ? "wss:" : "ws:";
|
||||
const uri =
|
||||
protocol +
|
||||
"//" +
|
||||
loc.hostname +
|
||||
(loc.port ? ":" + loc.port : "") +
|
||||
loc.pathname +
|
||||
"ws";
|
||||
const ws = new WebSocket(uri),
|
||||
self = this;
|
||||
ws.onerror = function () {
|
||||
ws.close();
|
||||
};
|
||||
ws.onclose = function () {
|
||||
window.setTimeout(self.connect, 1000);
|
||||
};
|
||||
ws.onmessage = function (evt) {
|
||||
try {
|
||||
var msg = JSON.parse(evt.data);
|
||||
store.update(msg);
|
||||
} catch (e) {
|
||||
window.toasts.error(e, evt.data);
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.compact = urlParams.get("compact");
|
||||
this.connect(); // websocket listener
|
||||
},
|
||||
};
|
||||
</script>
|
||||
95
assets/js/views/Config.vue
Normal file
95
assets/js/views/Config.vue
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<!-- <h1 class="display-4 pt-3 mx-auto text-center">Konfiguration</h1>
|
||||
<p class="lead mx-auto text-center">Details der Fahrzeug-, Wallbox- und Zählerkonfiguration.</p> -->
|
||||
|
||||
<div class="row mt-4 border-bottom">
|
||||
<div class="col-12">
|
||||
<p class="h1">{{ state.title || "Home" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row h5">
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Netzzähler:
|
||||
<span class="text-primary" v-if="state.gridConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
PV Zähler:
|
||||
<span class="text-primary" v-if="state.pvConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Batteriezähler:
|
||||
<span class="text-primary" v-if="state.batteryConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(loadpoint, id) in state.loadpoints"
|
||||
v-bind:loadpoint="loadpoint"
|
||||
:key="id"
|
||||
:id="'loadpoint-' + id"
|
||||
>
|
||||
<div class="row mt-4 border-bottom">
|
||||
<div class="col-12">
|
||||
<p class="h1">{{ loadpoint.title || "Ladepunkt" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row h5">
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Ladezähler:
|
||||
<span class="text-primary" v-if="loadpoint.chargeConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Phasen:
|
||||
<span class="text-primary">{{ loadpoint.phases }}p</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Min. Strom:
|
||||
<span class="text-primary">{{ loadpoint.minCurrent }}A</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Max. Strom:
|
||||
<span class="text-primary">{{ loadpoint.maxCurrent }}A</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row h5">
|
||||
<div class="col-md-4"></div>
|
||||
<div class="col-md-8 h2">
|
||||
<div class="row py-3 h2 border-bottom">
|
||||
<div class="col-12">Fahrzeug</div>
|
||||
</div>
|
||||
<div class="row h5">
|
||||
<div class="col-6 py-3">
|
||||
Modell:
|
||||
<span class="text-primary">{{ loadpoint.socTitle || "—" }}</span>
|
||||
</div>
|
||||
<div class="col-6 py-3">
|
||||
Kapazität:
|
||||
<span class="text-primary">{{ loadpoint.socCapacity }}kWh</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Config",
|
||||
data: function () {
|
||||
return {
|
||||
state: this.$root.$data.store.state, // global state
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
107
assets/js/views/Main.vue
Normal file
107
assets/js/views/Main.vue
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<Site v-bind:state="state" v-if="configured"></Site>
|
||||
<div v-else>
|
||||
<div class="row py-5">
|
||||
<div class="col12">
|
||||
<p class="h1 pt-5 pb-2 border-bottom">Willkommen bei evcc</p>
|
||||
<p class="lead pt-2">
|
||||
<b>evcc</b> ist dient zur flexiblen Ladesteuerung von Elektrofahrzeugen.
|
||||
</p>
|
||||
<p class="pt-2">
|
||||
Es sieht aus, als wäre Dein <b>evcc</b> noch nicht konfiguriert. Um
|
||||
<b>evcc</b> zu konfigurieren sind die folgenden Schritte notwendig:
|
||||
</p>
|
||||
<ol class="pt-2">
|
||||
<li>
|
||||
Erzeugen einer Konfigurationsdatei mit Namen
|
||||
<code>evcc.yaml</code>. Die Standardkonfiguration
|
||||
<code>evcc.dist.yaml</code> kann dafür als Vorlage dienen (<a
|
||||
href="https://github.com/andig/evcc/blob/master/evcc.dist.yaml"
|
||||
>Download</a
|
||||
>).
|
||||
</li>
|
||||
<li>Konfiguration der Wallbox als <code>chargers</code>.</li>
|
||||
<li>
|
||||
Konfiguration des EVU Zählers und evtl. weiterer Zähler unter
|
||||
<code>meters</code>.
|
||||
</li>
|
||||
<li>
|
||||
Konfiguration des Netzanschlusses unter
|
||||
<code>site</code>. In einer Site wird der Netzanschluss mit dem
|
||||
konfigurierten EVU Zähler (<code>meter</code>) verbunden.
|
||||
</li>
|
||||
<li>
|
||||
Konfiguration eines Ladepunktes unter
|
||||
<code>loadpoints</code>. In einem Ladepunkt wird die konfigurierte
|
||||
Wallbox (<code>charger</code>) mit dem Ladepunkt verbunden.
|
||||
</li>
|
||||
<li>
|
||||
Start von <b>evcc</b> mit der neu erstellten Konfiguration:
|
||||
<code>evcc -c evcc.yaml</code>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Minimale Beispielkonfiguration für <b>evcc</b>:</p>
|
||||
<p>
|
||||
<code>
|
||||
<pre class="mx-3">
|
||||
uri: localhost:7070 # Adresse für UI
|
||||
interval: 10s # Regelintervall
|
||||
meters:
|
||||
- name: evu-zähler
|
||||
type: ... # Detailkonfiguration des EVU Zählers
|
||||
- name: ladezähler
|
||||
type: ... # Detailkonfiguration des Ladezählers (optional)
|
||||
chargers:
|
||||
- name: wallbox
|
||||
type: ... # Detailkonfiguration der Wallbox
|
||||
site:
|
||||
title: Home
|
||||
meters:
|
||||
grid: evu-zähler # EVU Zähler
|
||||
loadpoints:
|
||||
- title: Ladepunkt # ui display name
|
||||
charger: wallbox # charger
|
||||
meters:
|
||||
charge: ladezähler # Ladezählers (optional)
|
||||
</pre
|
||||
>
|
||||
</code>
|
||||
</p>
|
||||
<p>
|
||||
Viel Spass mit <b>evcc</b>! Bei Problemen kannst Du uns auf
|
||||
<a href="https://github.com/andig/evcc/issues">GitHub</a>
|
||||
erreichen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Site from "../components/Site";
|
||||
|
||||
export default {
|
||||
name: "Main",
|
||||
components: { Site },
|
||||
data: function () {
|
||||
return {
|
||||
state: this.$root.$data.store.state, // global state
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
configured: function () {
|
||||
const val = window.evcc.configured;
|
||||
// for development purposes
|
||||
if (val == window.evcc.configured) {
|
||||
return true;
|
||||
}
|
||||
if (!isNaN(parseInt(val)) && parseInt(val) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
3819
package-lock.json
generated
3819
package-lock.json
generated
File diff suppressed because it is too large
Load diff
31
package.json
31
package.json
|
|
@ -6,14 +6,29 @@
|
|||
"dev": "rimraf dist; parcel watch assets/* --public-url ./dist/",
|
||||
"build": "rimraf dist; parcel build assets/* --public-url ./dist/",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"lint": "prettier assets --write && eslint assets/**/*.js assets/**/*.vue --fix",
|
||||
"fix-audit": "npm-force-resolutions"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@vue/component-compiler-utils": "^3.2.0",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"eslint": "^7.14.0",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.1.0",
|
||||
"eslint-plugin-vue": "^7.1.0",
|
||||
"husky": "^4.3.0",
|
||||
"lint-staged": "^10.5.1",
|
||||
"npm-force-resolutions": "0.0.3",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"prettier": "^2.2.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"npm-force-resolutions": "0.0.3"
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
},
|
||||
"engines": {
|
||||
"npm": "^6.0.0",
|
||||
|
|
@ -33,9 +48,23 @@
|
|||
"jquery": "^3.5.1",
|
||||
"popper.js": "^1.16.1",
|
||||
"vue": "^2.6.12",
|
||||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-router": "^3.4.7"
|
||||
},
|
||||
"resolutions": {
|
||||
"node-forge": "0.10.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"assets/**/*.{js,vue,json}": [
|
||||
"eslint --fix"
|
||||
],
|
||||
"assets/**/*.{html,js,vue,json,css}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26011
server/assets.go
26011
server/assets.go
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue