Introduce new UI design; upgrade to Vue 3 tech stack
BIN
.DS_Store
vendored
Normal file
|
|
@ -6,9 +6,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:vue/recommended",
|
"plugin:vue/vue3-recommended",
|
||||||
"plugin:prettier/recommended",
|
"plugin:prettier/recommended",
|
||||||
"prettier",
|
"prettier",
|
||||||
|
"plugin:storybook/recommended",
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
"vue/require-default-prop": "off",
|
"vue/require-default-prop": "off",
|
||||||
|
|
|
||||||
5
.github/workflows/default.yml
vendored
|
|
@ -72,6 +72,11 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
|
||||||
- name: Cache NPM modules
|
- name: Cache NPM modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
1
.gitignore
vendored
|
|
@ -24,3 +24,4 @@ ca-cert.srl
|
||||||
server-key.pem
|
server-key.pem
|
||||||
ca-key.pem
|
ca-key.pem
|
||||||
cmd/wip
|
cmd/wip
|
||||||
|
fly.toml
|
||||||
|
|
@ -1,4 +1,23 @@
|
||||||
|
const { loadConfigFromFile } = require("vite");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
stories: ["../assets/js/**/*.stories.mdx", "../assets/js/**/*.stories.@(js|jsx|ts|tsx)"],
|
stories: ["../assets/js/**/*.stories.mdx", "../assets/js/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||||
addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-postcss"],
|
addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-postcss"],
|
||||||
|
framework: "@storybook/vue3",
|
||||||
|
core: {
|
||||||
|
builder: "storybook-builder-vite",
|
||||||
|
},
|
||||||
|
staticDirs: ["../assets"],
|
||||||
|
async viteFinal(config, b) {
|
||||||
|
const { config: userConfig } = await loadConfigFromFile(
|
||||||
|
path.resolve(__dirname, "../vite.config.ts")
|
||||||
|
);
|
||||||
|
const vuePlugin = userConfig.plugins.find((p) => p.name === "vite:vue");
|
||||||
|
|
||||||
|
const vuePluginIndex = config.plugins.findIndex((p) => p.name === "vite:vue");
|
||||||
|
config.plugins[vuePluginIndex] = vuePlugin;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
<link href="/css/bootstrap.min.css" rel="stylesheet" />
|
|
||||||
<script src="/js/bootstrap.min.js" async></script>
|
|
||||||
|
|
||||||
<link href="/css/app.css" rel="stylesheet" />
|
|
||||||
|
|
@ -1,4 +1,26 @@
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
|
import "bootstrap";
|
||||||
|
import smoothscroll from "smoothscroll-polyfill";
|
||||||
|
import { app } from "@storybook/vue3";
|
||||||
|
import i18n from "../assets/js/i18n";
|
||||||
|
import "../assets/css/app.css";
|
||||||
|
|
||||||
|
smoothscroll.polyfill();
|
||||||
|
app.use(i18n);
|
||||||
|
|
||||||
export const parameters = {
|
export const parameters = {
|
||||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||||
}
|
backgrounds: {
|
||||||
|
default: "dark",
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
name: "light",
|
||||||
|
value: "#ffffff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dark",
|
||||||
|
value: "#28293e",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ RUN make install-ui
|
||||||
|
|
||||||
# build ui
|
# build ui
|
||||||
COPY assets assets
|
COPY assets assets
|
||||||
COPY vue.config.js vue.config.js
|
COPY vite.config.js vite.config.js
|
||||||
COPY .eslintrc.js .eslintrc.js
|
COPY .eslintrc.js .eslintrc.js
|
||||||
RUN make clean ui
|
RUN make clean ui
|
||||||
|
|
||||||
|
|
|
||||||
41
README.md
|
|
@ -41,14 +41,51 @@ Evcc is an extensible EV Charge Controller with PV integration implemented in [G
|
||||||
|
|
||||||
You'll find everything you need in our [documentation](https://docs.evcc.io/) (German).
|
You'll find everything you need in our [documentation](https://docs.evcc.io/) (German).
|
||||||
|
|
||||||
## Build
|
## Contribute
|
||||||
|
|
||||||
To build EVCC from source, [Go][2] 1.18 and [Node][3] 16 are required:
|
To build evcc from source, [Go][2] 1.18 and [Node][3] 16 are required.
|
||||||
|
|
||||||
|
Build and run go backend. The UI becomes available at http://127.0.0.1:7070/
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make
|
make
|
||||||
|
./evcc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the livelreloading development server. It pulls its data from port 7070 (see above).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code formatting
|
||||||
|
|
||||||
|
We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. For VSCode use the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Veture](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make lint
|
||||||
|
make lint-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changing UI code
|
||||||
|
|
||||||
|
To ensure reproducability the build frontend artifacts are part of the source code repository. If you've made changes to frontend code, please make sure to rebuild the production assets before you commit.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make ui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changing templates
|
||||||
|
|
||||||
|
Evcc supports a massive amount of different devices. To keep our documentation and website in sync with the latest software the core project (this repo) generates meta-data that's pushed to the `docs` and `evcc.io` repository. Make sure to update this meta-data every time you make changes to a templates.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make docs
|
||||||
|
```
|
||||||
|
|
||||||
|
If you miss one of the above steps Gitub Actions will likely trigger a **Porcelain** error.
|
||||||
|
|
||||||
## Sponsorship
|
## Sponsorship
|
||||||
|
|
||||||
Evcc believes in open source software. We're committed to provide best in class EV charging experience.
|
Evcc believes in open source software. We're committed to provide best in class EV charging experience.
|
||||||
|
|
|
||||||
BIN
assets/.DS_Store
vendored
Normal file
|
|
@ -1,14 +1,67 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "Montserrat";
|
||||||
|
src: url("../font/Montserrat-Medium.woff2") format("woff2");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Montserrat";
|
||||||
|
src: url("../font/Montserrat-Bold.woff2") format("woff2");
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--evcc-green: #66d85a;
|
--evcc-green: #baffcb;
|
||||||
--evcc-dark-green: #3aba2c;
|
--evcc-dark-green: #0fde41;
|
||||||
--evcc-yellow: #ffe000;
|
--evcc-yellow: #faf000;
|
||||||
|
--bs-gray-dark: #28293e;
|
||||||
|
--bs-gray-medium: #93949e;
|
||||||
|
|
||||||
--evcc-grid: var(--bs-gray-dark);
|
--evcc-grid: var(--bs-gray-dark);
|
||||||
--evcc-self: var(--evcc-dark-green);
|
--evcc-self: var(--evcc-dark-green);
|
||||||
--evcc-export: var(--evcc-yellow);
|
--evcc-export: var(--evcc-yellow);
|
||||||
|
|
||||||
|
--evcc-transition-slow: 1000ms;
|
||||||
|
--evcc-transition-medium: 500ms;
|
||||||
|
--evcc-transition-fast: 250ms;
|
||||||
|
--evcc-transition-very-fast: 100ms;
|
||||||
|
|
||||||
--bs-primary: var(--evcc-dark-green);
|
--bs-primary: var(--evcc-dark-green);
|
||||||
--bs-primary-rgb: 58, 186, 44;
|
--bs-primary-rgb: 58, 186, 44;
|
||||||
|
|
||||||
|
--bs-body-font-size: 14px;
|
||||||
|
--bs-font-sans-serif: Montserrat, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
|
||||||
|
Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||||
|
"Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
/* prevents content jump when scrollbar is activate */
|
||||||
|
width: 100vw;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--evcc-gray-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-primary {
|
.bg-primary {
|
||||||
|
|
@ -27,22 +80,33 @@ a {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rounded {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary,
|
.btn-primary,
|
||||||
.btn-primary:focus {
|
.btn-primary:focus {
|
||||||
background-color: var(--bs-gray-dark);
|
background-color: var(--bs-gray-dark);
|
||||||
border-color: var(--bs-gray-dark);
|
border-color: var(--bs-gray-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover,
|
.btn-primary:hover,
|
||||||
.btn-primary:active {
|
.btn-primary:active {
|
||||||
background-color: var(--evcc-dark-green);
|
background-color: var(--evcc-dark-green);
|
||||||
border-color: var(--evcc-dark-green);
|
border-color: var(--evcc-dark-green);
|
||||||
}
|
}
|
||||||
.btn-outline-primary {
|
.btn-outline-primary,
|
||||||
|
.btn-outline-primary:focus {
|
||||||
color: var(--bs-gray-dark);
|
color: var(--bs-gray-dark);
|
||||||
|
background-color: transparent;
|
||||||
|
border-width: 2px;
|
||||||
border-color: var(--bs-gray-dark);
|
border-color: var(--bs-gray-dark);
|
||||||
}
|
}
|
||||||
.btn-outline-primary:hover {
|
.btn-outline-primary:hover,
|
||||||
background-color: var(--evcc-dark-green);
|
.btn-outline-primary:active {
|
||||||
|
color: var(--evcc-dark-green);
|
||||||
|
background-color: transparent;
|
||||||
|
border-width: 2px;
|
||||||
border-color: var(--evcc-dark-green);
|
border-color: var(--evcc-dark-green);
|
||||||
}
|
}
|
||||||
.text-evcc {
|
.text-evcc {
|
||||||
|
|
@ -52,6 +116,18 @@ a {
|
||||||
.text-grid {
|
.text-grid {
|
||||||
color: var(--evcc-grid);
|
color: var(--evcc-grid);
|
||||||
}
|
}
|
||||||
|
.text-dark {
|
||||||
|
color: var(--bs-gray-dark);
|
||||||
|
}
|
||||||
|
.text-gray {
|
||||||
|
color: var(--bs-gray-medium);
|
||||||
|
}
|
||||||
|
.text-gray-medium {
|
||||||
|
color: var(--bs-gray-medium);
|
||||||
|
}
|
||||||
|
.bg-dark {
|
||||||
|
background-color: var(--bs-gray-dark) !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 400px) and (max-width: 574px) {
|
@media screen and (min-width: 400px) and (max-width: 574px) {
|
||||||
.d-xs-none {
|
.d-xs-none {
|
||||||
|
|
@ -62,6 +138,33 @@ a {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-backdrop.show {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
@ -69,3 +172,13 @@ a {
|
||||||
.v-popper__inner {
|
.v-popper__inner {
|
||||||
margin: 0 12px !important;
|
margin: 0 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.small,
|
||||||
|
small {
|
||||||
|
font-size: var(--bs-body-font-size);
|
||||||
|
color: var(--bs-gray-medium) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
|
||||||
BIN
assets/font/Montserrat-Bold.woff2
Normal file
BIN
assets/font/Montserrat-Medium.woff2
Normal file
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square70x70logo src="ico/mstile-70x70.png" />
|
|
||||||
<square150x150logo src="ico/mstile-150x150.png" />
|
|
||||||
<square310x310logo src="ico/mstile-310x310.png" />
|
|
||||||
<TileColor>#18191a</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
||||||
|
|
@ -3,22 +3,21 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="description" content="EV Charge Controller" />
|
<meta name="description" content="evcc - Sonne tanken ☀️🚘" />
|
||||||
<meta name="author" content="andig" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="ico/apple-touch-icon.png?[[.Version]]" />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png?[[.Version]]" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png?[[.Version]]" />
|
|
||||||
<link rel="manifest" href="ico/site.webmanifest" />
|
|
||||||
<link rel="mask-icon" href="ico/safari-pinned-tab.svg?[[.Version]]" color="#18191a" />
|
|
||||||
<link rel="shortcut icon" href="ico/favicon.ico?[[.Version]]" />
|
|
||||||
<meta name="apple-mobile-web-app-title" content="evcc" />
|
<meta name="apple-mobile-web-app-title" content="evcc" />
|
||||||
<meta name="application-name" content="evcc" />
|
<meta name="application-name" content="evcc" />
|
||||||
<meta name="msapplication-TileColor" content="#18191a" />
|
|
||||||
<meta name="msapplication-config" content="ico/browserconfig.xml" />
|
<link rel="apple-touch-icon" sizes="180x180" href="meta/apple-touch-icon.png?[[.Version]]" />
|
||||||
<meta name="theme-color" content="#18191a" />
|
<link rel="icon" type="image/png" sizes="32x32" href="meta/favicon-32x32.png?[[.Version]]" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="meta/favicon-16x16.png?[[.Version]]" />
|
||||||
|
<link rel="manifest" href="meta/site.webmanifest?[[.Version]]" />
|
||||||
|
<link rel="mask-icon" href="meta/safari-pinned-tab.svg?[[.Version]]" color="#28293e" />
|
||||||
|
<link rel="shortcut icon" href="meta/favicon.ico?[[.Version]]" />
|
||||||
|
<meta name="msapplication-TileColor" content="#28293e" />
|
||||||
|
<meta name="msapplication-config" content="meta/browserconfig.xml?[[.Version]]" />
|
||||||
|
<meta name="theme-color" content="#28293e" />
|
||||||
|
|
||||||
<title>evcc</title>
|
<title>evcc</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -32,5 +31,6 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
import "bootstrap/dist/css/bootstrap.min.css";
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
import "bootstrap";
|
import "bootstrap";
|
||||||
|
import smoothscroll from "smoothscroll-polyfill";
|
||||||
import "../css/app.css";
|
import "../css/app.css";
|
||||||
import Vue from "vue";
|
import { createApp, h } from "vue";
|
||||||
import VueMeta from "vue-meta";
|
import { createMetaManager, plugin as metaPlugin } from "vue-meta";
|
||||||
import api from "./api";
|
import api from "./api";
|
||||||
import App from "./views/App";
|
import App from "./views/App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import i18n from "./i18n";
|
import i18n from "./i18n";
|
||||||
import "./tooltip";
|
import featureflags from "./featureflags";
|
||||||
import store from "./store";
|
|
||||||
|
|
||||||
Vue.use(VueMeta);
|
smoothscroll.polyfill();
|
||||||
|
|
||||||
window.app = new Vue({
|
const app = createApp({
|
||||||
el: "#app",
|
data() {
|
||||||
router,
|
return { notifications: [] };
|
||||||
i18n,
|
},
|
||||||
data: { store, notifications: [] },
|
|
||||||
methods: {
|
methods: {
|
||||||
raise: function (msg) {
|
raise: function (msg) {
|
||||||
console[msg.type](msg);
|
console[msg.type](msg);
|
||||||
|
|
@ -48,11 +47,18 @@ window.app = new Vue({
|
||||||
this.raise(msg);
|
this.raise(msg);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: function (h) {
|
render: function () {
|
||||||
return h(App, { props: { notifications: this.notifications } });
|
return h(App, { notifications: this.notifications });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(i18n);
|
||||||
|
app.use(router);
|
||||||
|
app.use(createMetaManager());
|
||||||
|
app.use(metaPlugin);
|
||||||
|
app.use(featureflags);
|
||||||
|
window.app = app.mount("#app");
|
||||||
|
|
||||||
window.setInterval(function () {
|
window.setInterval(function () {
|
||||||
api.get("health").catch(function () {
|
api.get("health").catch(function () {
|
||||||
window.app.error({ message: "Server unavailable" });
|
window.app.error({ message: "Server unavailable" });
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{ 'power--in': charge, 'power--out': discharge }">
|
<component :is="`shopicon-regular-${icon}`"></component>
|
||||||
<fa-icon class="battery" :icon="batteryIcon"></fa-icon
|
|
||||||
><fa-icon class="arrow" icon="angle-double-right"></fa-icon>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import "../../icons";
|
import "@h2d2/shopicons/es/regular/batteryfull";
|
||||||
|
import "@h2d2/shopicons/es/regular/batterythreequarters";
|
||||||
|
import "@h2d2/shopicons/es/regular/batteryhalf";
|
||||||
|
import "@h2d2/shopicons/es/regular/batteryquarter";
|
||||||
|
import "@h2d2/shopicons/es/regular/batteryempty";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "BatteryIcon",
|
name: "BatteryIcon",
|
||||||
props: {
|
props: {
|
||||||
discharge: { type: Boolean },
|
|
||||||
charge: { type: Boolean },
|
|
||||||
soc: { type: Number, default: 0 },
|
soc: { type: Number, default: 0 },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
batteryIcon: function () {
|
icon: function () {
|
||||||
if (this.soc > 80) return "battery-full";
|
if (this.soc > 80) return "batteryfull";
|
||||||
if (this.soc > 60) return "battery-three-quarters";
|
if (this.soc > 60) return "batterythreequarters";
|
||||||
if (this.soc > 40) return "battery-half";
|
if (this.soc > 40) return "batteryhalf";
|
||||||
if (this.soc > 20) return "battery-quarter";
|
if (this.soc > 20) return "batteryquarter";
|
||||||
return "battery-empty";
|
return "batteryempty";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
|
||||||
.battery {
|
|
||||||
transform: translateX(0.35rem) rotate(-90deg);
|
|
||||||
transition-property: transform;
|
|
||||||
transition-duration: 250ms;
|
|
||||||
transition-timing-function: ease;
|
|
||||||
}
|
|
||||||
.power--in .battery {
|
|
||||||
transform: translateX(0.7rem) rotate(-90deg);
|
|
||||||
}
|
|
||||||
.power--out .battery {
|
|
||||||
transform: translateX(0) rotate(-90deg);
|
|
||||||
}
|
|
||||||
.arrow {
|
|
||||||
margin-left: -0.2rem;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-0.5rem);
|
|
||||||
transition-property: opacity, transform;
|
|
||||||
transition-duration: 250ms;
|
|
||||||
transition-timing-function: ease;
|
|
||||||
}
|
|
||||||
.power--in .arrow {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(-1rem) scaleX(1);
|
|
||||||
}
|
|
||||||
.power--out .arrow {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0rem) scaleX(1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -1,228 +0,0 @@
|
||||||
/* globals describe, it, expect */
|
|
||||||
import { shallowMount } from "@vue/test-utils";
|
|
||||||
import Energyflow from "./Energyflow.vue";
|
|
||||||
|
|
||||||
const mocks = {
|
|
||||||
$t: (x) => x,
|
|
||||||
$tc: (x, n) => `${n} ${x}`,
|
|
||||||
$n: function (value, options) {
|
|
||||||
const n = new Intl.NumberFormat("en-EN", options);
|
|
||||||
return n.format(value);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("Energyflow.vue", () => {
|
|
||||||
const defaultProps = {
|
|
||||||
gridConfigured: true,
|
|
||||||
gridPower: 0,
|
|
||||||
pvConfigured: true,
|
|
||||||
pvPower: 0,
|
|
||||||
homePower: 0,
|
|
||||||
batteryConfigured: false,
|
|
||||||
batteryPower: 0,
|
|
||||||
batterySoC: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
it("using pv and grid power", async () => {
|
|
||||||
const wrapper = shallowMount(Energyflow, {
|
|
||||||
mocks,
|
|
||||||
propsData: {
|
|
||||||
...defaultProps,
|
|
||||||
gridPower: 1000,
|
|
||||||
pvPower: 4000,
|
|
||||||
homePower: 1300,
|
|
||||||
loadpointsPower: 3700,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await wrapper.find(".energyflow").trigger("click");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-grid-import]").text()).toMatch("1.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("4.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0.0 kW");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-home-power]").text()).toMatch("1.3 kW");
|
|
||||||
expect(wrapper.find("[data-test-loadpoints]").text()).toMatch("3.7 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-production]").text()).toMatch("4.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-battery]").exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("exporting all pv power, no usage", async () => {
|
|
||||||
const wrapper = shallowMount(Energyflow, {
|
|
||||||
mocks,
|
|
||||||
propsData: { ...defaultProps, gridPower: -4000, pvPower: 5000, loadpointsPower: 1000 },
|
|
||||||
});
|
|
||||||
|
|
||||||
await wrapper.find(".energyflow").trigger("click");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-grid-import]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("1.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-export]").text()).toMatch("4.0 kW");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-home-power]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-loadpoints]").text()).toMatch("1.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-production]").text()).toMatch("5.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-battery]").exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("more grid export than pv, grid value wins (invalid state)", async () => {
|
|
||||||
const wrapper = shallowMount(Energyflow, {
|
|
||||||
mocks,
|
|
||||||
propsData: { ...defaultProps, gridPower: -4000, pvPower: 3000 },
|
|
||||||
});
|
|
||||||
|
|
||||||
await wrapper.find(".energyflow").trigger("click");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-grid-import]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-export]").text()).toMatch("4.0 kW");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-home-power]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-loadpoints]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-production]").text()).toMatch("3.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-battery]").exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("more loadpoints power than grid import, loadpoints unchanged, house consumption min 0 (invalid state)", async () => {
|
|
||||||
const wrapper = shallowMount(Energyflow, {
|
|
||||||
mocks,
|
|
||||||
propsData: { ...defaultProps, gridPower: 6000, pvPower: 0, loadpointsPower: 7000 },
|
|
||||||
});
|
|
||||||
|
|
||||||
await wrapper.find(".energyflow").trigger("click");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-grid-import]").text()).toMatch("6.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0.0 kW");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-home-power]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-loadpoints]").text()).toMatch("7.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-production]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-battery]").exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("only grid usage, no pv, idleBattery", async () => {
|
|
||||||
const wrapper = shallowMount(Energyflow, {
|
|
||||||
mocks,
|
|
||||||
propsData: {
|
|
||||||
...defaultProps,
|
|
||||||
gridPower: 360,
|
|
||||||
pvPower: 0,
|
|
||||||
batteryConfigured: true,
|
|
||||||
batteryPower: 0,
|
|
||||||
homePower: 360,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await wrapper.find(".energyflow").trigger("click");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-grid-import]").text()).toMatch("360 W");
|
|
||||||
expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("0 W");
|
|
||||||
expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0 W");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-home-power]").text()).toMatch("360 W");
|
|
||||||
expect(wrapper.find("[data-test-pv-production]").text()).toMatch("0 W");
|
|
||||||
expect(wrapper.find("[data-test-battery]").text()).toMatch("main.energyflow.battery");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("grid and battery usage, no pv", async () => {
|
|
||||||
const wrapper = shallowMount(Energyflow, {
|
|
||||||
mocks,
|
|
||||||
propsData: {
|
|
||||||
...defaultProps,
|
|
||||||
gridPower: 300,
|
|
||||||
batteryConfigured: true,
|
|
||||||
batteryPower: 234,
|
|
||||||
batterySoC: 77,
|
|
||||||
pvPower: 0,
|
|
||||||
homePower: 534,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await wrapper.find(".energyflow").trigger("click");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-grid-import]").text()).toMatch("300 W");
|
|
||||||
expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("234 W");
|
|
||||||
expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0 W");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-home-power]").text()).toMatch("534 W");
|
|
||||||
expect(wrapper.find("[data-test-pv-production]").text()).toMatch("0 W");
|
|
||||||
expect(wrapper.find("[data-test-battery]").text()).toMatch("234 W");
|
|
||||||
expect(wrapper.find("[data-test-battery]").text()).toMatch("77%");
|
|
||||||
expect(wrapper.find("[data-test-battery]").text()).toMatch("main.energyflow.batteryDischarge");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("battery charge, pv export", async () => {
|
|
||||||
const wrapper = shallowMount(Energyflow, {
|
|
||||||
mocks,
|
|
||||||
propsData: {
|
|
||||||
...defaultProps,
|
|
||||||
gridPower: -2500,
|
|
||||||
batteryConfigured: true,
|
|
||||||
batteryPower: -1700,
|
|
||||||
pvPower: 9000,
|
|
||||||
homePower: 4800,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await wrapper.find(".energyflow").trigger("click");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-grid-import]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("6.5 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-export]").text()).toMatch("2.5 kW");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-home-power]").text()).toMatch("4.8 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-production]").text()).toMatch("9.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-battery]").text()).toMatch("1.7 kW");
|
|
||||||
expect(wrapper.find("[data-test-battery]").text()).toMatch("main.energyflow.batteryCharge");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("thresholds", async () => {
|
|
||||||
const wrapper = shallowMount(Energyflow, {
|
|
||||||
mocks,
|
|
||||||
propsData: {
|
|
||||||
...defaultProps,
|
|
||||||
gridPower: 5555,
|
|
||||||
batteryConfigured: true,
|
|
||||||
batteryPower: 1234,
|
|
||||||
pvPower: 378,
|
|
||||||
homePower: 7200,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await wrapper.find(".energyflow").trigger("click");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-grid-import]").text()).toMatch("5.6 kW");
|
|
||||||
expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("1.6 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0.0 kW");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-home-power]").text()).toMatch("7.2 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-production]").text()).toMatch("0.4 kW");
|
|
||||||
expect(wrapper.find("[data-test-battery]").text()).toMatch("1.2 kW");
|
|
||||||
expect(wrapper.find("[data-test-battery]").text()).toMatch("main.energyflow.batteryDischarge");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("battery charge, grid import", async () => {
|
|
||||||
const wrapper = shallowMount(Energyflow, {
|
|
||||||
mocks,
|
|
||||||
propsData: {
|
|
||||||
...defaultProps,
|
|
||||||
gridPower: 1500,
|
|
||||||
batteryConfigured: true,
|
|
||||||
batteryPower: -1000,
|
|
||||||
homePower: 500,
|
|
||||||
pvPower: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await wrapper.find(".energyflow").trigger("click");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-grid-import]").text()).toMatch("1.5 kW");
|
|
||||||
expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0.0 kW");
|
|
||||||
|
|
||||||
expect(wrapper.find("[data-test-home-power]").text()).toMatch("0.5 kW");
|
|
||||||
expect(wrapper.find("[data-test-pv-production]").text()).toMatch("0.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-battery]").text()).toMatch("1.0 kW");
|
|
||||||
expect(wrapper.find("[data-test-battery]").text()).toMatch("main.energyflow.batteryCharge");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import Energyflow from "./";
|
import Energyflow from "./";
|
||||||
import i18n from "../../i18n";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Main/Energyflow",
|
title: "Main/Energyflow",
|
||||||
|
|
@ -13,13 +12,19 @@ export default {
|
||||||
batteryPower: { control: { type: "range", min: -4000, max: 4000, step: 100 } },
|
batteryPower: { control: { type: "range", min: -4000, max: 4000, step: 100 } },
|
||||||
batterySoC: { control: { type: "range", min: 0, max: 100, step: 1 } },
|
batterySoC: { control: { type: "range", min: 0, max: 100, step: 1 } },
|
||||||
},
|
},
|
||||||
|
parameters: {
|
||||||
|
backgrounds: {
|
||||||
|
default: "light",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Template = (args, { argTypes }) => ({
|
const Template = (args) => ({
|
||||||
i18n,
|
setup() {
|
||||||
props: Object.keys(argTypes),
|
return { args };
|
||||||
|
},
|
||||||
components: { Energyflow },
|
components: { Energyflow },
|
||||||
template: '<Energyflow v-bind="$props"></Energyflow>',
|
template: '<Energyflow v-bind="args"></Energyflow>',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GridAndPV = Template.bind({});
|
export const GridAndPV = Template.bind({});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div
|
||||||
<div
|
class="energyflow cursor-pointer position-relative"
|
||||||
class="row align-items-start align-items-md-center mt-4 energyflow"
|
:class="{ 'energyflow--open': detailsOpen }"
|
||||||
@click="toggleDetails"
|
@click="toggleDetails"
|
||||||
>
|
>
|
||||||
|
<div class="row">
|
||||||
<Visualization
|
<Visualization
|
||||||
class="col-12 offset-md-1 col-md-6 offset-lg-1 col-lg-8 offset-xl-1 col-xl-6 offset-xxl-1 col-xl-8 order-md-2"
|
class="col-12 mb-3 mb-md-4"
|
||||||
:showDetails="showDetails"
|
|
||||||
:gridImport="gridImport"
|
:gridImport="gridImport"
|
||||||
:selfConsumption="selfConsumption"
|
:selfConsumption="selfConsumption"
|
||||||
:loadpoints="loadpointsPower"
|
:loadpoints="loadpointsPower"
|
||||||
|
|
@ -18,88 +18,101 @@
|
||||||
:batterySoC="batterySoC"
|
:batterySoC="batterySoC"
|
||||||
:valuesInKw="valuesInKw"
|
:valuesInKw="valuesInKw"
|
||||||
/>
|
/>
|
||||||
<div
|
</div>
|
||||||
class="col-12 col-sm-6 col-md-5 col-lg-3 col-xl-3 order-md-1 mt-2 mt-md-0"
|
<div class="indicator position-absolute bottom-0 start-50">
|
||||||
:class="`${showDetails ? 'd-block' : `d-none d-md-block`}`"
|
<shopicon-regular-arrowdown></shopicon-regular-arrowdown>
|
||||||
>
|
</div>
|
||||||
<div class="d-flex justify-content-between" data-test-pv-production>
|
<div class="details" :style="{ height: detailsHeight }">
|
||||||
<span class="details-icon text-muted"><fa-icon icon="sun"></fa-icon></span>
|
<div ref="detailsInner" class="details-inner row">
|
||||||
<span class="text-nowrap flex-grow-1">{{
|
<div class="col-12 d-flex justify-content-between pt-2 mb-4">
|
||||||
$t("main.energyflow.pvProduction")
|
<div class="d-flex flex-nowrap align-items-center">
|
||||||
}}</span>
|
<span class="color-self me-2"
|
||||||
<span class="text-end text-nowrap ps-1">{{ kw(pvProduction) }}</span>
|
><shopicon-filled-square></shopicon-filled-square
|
||||||
</div>
|
></span>
|
||||||
<div class="d-flex justify-content-between" data-test-home-power>
|
<span>{{ $t("main.energyflow.selfConsumption") }}</span>
|
||||||
<span class="details-icon text-muted"><fa-icon icon="home"></fa-icon></span>
|
</div>
|
||||||
<span class="text-nowrap flex-grow-1">{{
|
<div v-if="gridImport > 0" class="d-flex flex-nowrap align-items-center">
|
||||||
$t("main.energyflow.homePower")
|
<span>{{ $t("main.energyflow.gridImport") }}</span>
|
||||||
}}</span>
|
<span class="color-grid ms-2"
|
||||||
<span class="text-end text-nowrap ps-1">{{ kw(homePower) }}</span>
|
><shopicon-filled-square></shopicon-filled-square
|
||||||
</div>
|
></span>
|
||||||
<div class="d-flex justify-content-between" data-test-loadpoints>
|
</div>
|
||||||
<span class="details-icon text-muted"><fa-icon icon="car"></fa-icon></span>
|
<div v-else class="d-flex flex-nowrap align-items-center">
|
||||||
<span class="text-nowrap flex-grow-1">{{
|
<span>{{ $t("main.energyflow.pvExport") }}</span>
|
||||||
$tc("main.energyflow.loadpoints", activeLoadpointsCount, {
|
<span class="color-export ms-2"
|
||||||
count: activeLoadpointsCount,
|
><shopicon-filled-square></shopicon-filled-square
|
||||||
})
|
></span>
|
||||||
}}</span>
|
</div>
|
||||||
<span class="text-end text-nowrap ps-1">{{ kw(loadpointsPower) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="batteryConfigured"
|
class="col-12 col-md-6 pe-md-5 pb-4 d-flex flex-column justify-content-between"
|
||||||
class="d-flex justify-content-between"
|
|
||||||
data-test-battery
|
|
||||||
>
|
>
|
||||||
<span class="details-icon text-muted">
|
<div class="d-flex justify-content-between align-items-end mb-4">
|
||||||
<BatteryIcon
|
<h3 class="m-0">In</h3>
|
||||||
:soc="batterySoC"
|
<span class="fw-bold text-gray-medium">{{ kw(inPower) }}</span>
|
||||||
:charge="batteryCharge > 0"
|
</div>
|
||||||
:discharge="batteryDischarge > 0"
|
<div>
|
||||||
|
<EnergyflowEntry
|
||||||
|
:name="$t('main.energyflow.pvProduction')"
|
||||||
|
icon="sun"
|
||||||
|
:power="pvProduction"
|
||||||
|
:valuesInKw="valuesInKw"
|
||||||
/>
|
/>
|
||||||
</span>
|
<EnergyflowEntry
|
||||||
<span class="text-nowrap flex-grow-1 text-truncate">
|
v-if="batteryConfigured"
|
||||||
<span v-if="batteryCharge">{{ $t("main.energyflow.batteryCharge") }}</span>
|
:name="$t('main.energyflow.batteryDischarge')"
|
||||||
<span v-else-if="batteryDischarge">{{
|
icon="battery"
|
||||||
$t("main.energyflow.batteryDischarge")
|
:soc="batterySoC"
|
||||||
}}</span>
|
:power="batteryDischarge"
|
||||||
<span v-else>{{ $t("main.energyflow.battery") }}</span>
|
:valuesInKw="valuesInKw"
|
||||||
</span>
|
/>
|
||||||
<span class="text-end text-nowrap ps-1">
|
<EnergyflowEntry
|
||||||
{{ batterySoC }}% /
|
:name="$t('main.energyflow.gridImport')"
|
||||||
{{ kw(Math.abs(batteryPower)) }}
|
icon="powersupply"
|
||||||
</span>
|
:power="gridImport"
|
||||||
|
:valuesInKw="valuesInKw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
class="col-12 col-md-6 ps-md-5 pb-4 d-flex flex-column justify-content-between"
|
||||||
v-if="showDetails"
|
>
|
||||||
class="col-12 col-sm-6 offset-md-6 col-md-6 offset-lg-4 col-lg-8 d-block d-md-flex order-md-3 justify-content-between mt-2"
|
<div class="d-flex justify-content-between align-items-end mb-4">
|
||||||
>
|
<h3 class="m-0">Out</h3>
|
||||||
<div class="text-nowrap d-flex d-md-block" data-test-grid-import>
|
<span class="fw-bold text-gray-medium">{{ kw(outPower) }}</span>
|
||||||
<span class="color-grid details-icon"><fa-icon icon="square"></fa-icon></span>
|
</div>
|
||||||
<span class="text-nowrap flex-grow-1">{{
|
<div>
|
||||||
$t("main.energyflow.gridImport")
|
<EnergyflowEntry
|
||||||
}}</span>
|
:name="$t('main.energyflow.homePower')"
|
||||||
<span class="text-end text-nowrap d-md-none">
|
icon="home"
|
||||||
{{ kw(gridImport) }}
|
:power="homePower"
|
||||||
</span>
|
:valuesInKw="valuesInKw"
|
||||||
</div>
|
/>
|
||||||
<div class="text-nowrap d-flex d-md-block" data-test-self-consumption>
|
<EnergyflowEntry
|
||||||
<span class="color-self details-icon"><fa-icon icon="square"></fa-icon></span>
|
:name="
|
||||||
<span class="text-nowrap flex-grow-1">{{
|
$tc('main.energyflow.loadpoints', activeLoadpointsCount, {
|
||||||
$t("main.energyflow.selfConsumption")
|
count: activeLoadpointsCount,
|
||||||
}}</span>
|
})
|
||||||
<span class="text-end text-nowrap d-md-none">
|
"
|
||||||
{{ kw(selfConsumption) }}
|
icon="car3"
|
||||||
</span>
|
:power="loadpointsPower"
|
||||||
</div>
|
:valuesInKw="valuesInKw"
|
||||||
<div class="text-nowrap d-flex d-md-block" data-test-pv-export>
|
/>
|
||||||
<span class="color-export details-icon"><fa-icon icon="square"></fa-icon></span>
|
<EnergyflowEntry
|
||||||
<span class="text-nowrap flex-grow-1">{{
|
v-if="batteryConfigured"
|
||||||
$t("main.energyflow.pvExport")
|
:name="$t('main.energyflow.batteryCharge')"
|
||||||
}}</span>
|
icon="battery"
|
||||||
<span class="text-end text-nowrap d-md-none">
|
:soc="batterySoC"
|
||||||
{{ kw(pvExport) }}
|
:power="batteryCharge"
|
||||||
</span>
|
:valuesInKw="valuesInKw"
|
||||||
|
/>
|
||||||
|
<EnergyflowEntry
|
||||||
|
:name="$t('main.energyflow.pvExport')"
|
||||||
|
icon="powersupply"
|
||||||
|
:power="pvExport"
|
||||||
|
:valuesInKw="valuesInKw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -107,14 +120,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import "../../icons";
|
import "@h2d2/shopicons/es/filled/square";
|
||||||
import formatter from "../../mixins/formatter";
|
import "@h2d2/shopicons/es/regular/arrowdown";
|
||||||
import Visualization from "./Visualization.vue";
|
import Visualization from "./Visualization.vue";
|
||||||
import BatteryIcon from "./BatteryIcon.vue";
|
import EnergyflowEntry from "./EnergyflowEntry.vue";
|
||||||
|
import formatter from "../../mixins/formatter";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Energyflow",
|
name: "Energyflow",
|
||||||
components: { Visualization, BatteryIcon },
|
components: { Visualization, EnergyflowEntry },
|
||||||
mixins: [formatter],
|
mixins: [formatter],
|
||||||
props: {
|
props: {
|
||||||
gridConfigured: Boolean,
|
gridConfigured: Boolean,
|
||||||
|
|
@ -128,8 +142,8 @@ export default {
|
||||||
batteryPower: { type: Number, default: 0 },
|
batteryPower: { type: Number, default: 0 },
|
||||||
batterySoC: { type: Number, default: 0 },
|
batterySoC: { type: Number, default: 0 },
|
||||||
},
|
},
|
||||||
data: function () {
|
data: () => {
|
||||||
return { showDetails: false };
|
return { detailsOpen: false, detailsCompleteHeight: null };
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
gridImport: function () {
|
gridImport: function () {
|
||||||
|
|
@ -143,10 +157,10 @@ export default {
|
||||||
return Math.abs(this.batteryPower) < batteryPowerThreshold ? 0 : this.batteryPower;
|
return Math.abs(this.batteryPower) < batteryPowerThreshold ? 0 : this.batteryPower;
|
||||||
},
|
},
|
||||||
batteryDischarge: function () {
|
batteryDischarge: function () {
|
||||||
return Math.max(0, this.batteryPowerAdjusted);
|
return Math.abs(Math.max(0, this.batteryPowerAdjusted));
|
||||||
},
|
},
|
||||||
batteryCharge: function () {
|
batteryCharge: function () {
|
||||||
return Math.min(0, this.batteryPowerAdjusted) * -1;
|
return Math.abs(Math.min(0, this.batteryPowerAdjusted) * -1);
|
||||||
},
|
},
|
||||||
selfConsumption: function () {
|
selfConsumption: function () {
|
||||||
const ownPower = this.batteryDischarge + this.pvProduction;
|
const ownPower = this.batteryDischarge + this.pvProduction;
|
||||||
|
|
@ -159,20 +173,60 @@ export default {
|
||||||
valuesInKw: function () {
|
valuesInKw: function () {
|
||||||
return this.gridImport + this.selfConsumption + this.pvExport > 1000;
|
return this.gridImport + this.selfConsumption + this.pvExport > 1000;
|
||||||
},
|
},
|
||||||
|
inPower: function () {
|
||||||
|
return this.gridImport + this.pvProduction + this.batteryDischarge;
|
||||||
|
},
|
||||||
|
outPower: function () {
|
||||||
|
return this.homePower + this.loadpointsPower + this.pvExport + this.batteryCharge;
|
||||||
|
},
|
||||||
|
detailsHeight: function () {
|
||||||
|
return this.detailsOpen ? this.detailsCompleteHeight + "px" : 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener("resize", this.updateHeight);
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
window.removeEventListener("resize", this.updateHeight);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
kw: function (watt) {
|
kw: function (watt) {
|
||||||
return this.fmtKw(watt, this.valuesInKw);
|
return this.fmtKw(watt, this.valuesInKw);
|
||||||
},
|
},
|
||||||
toggleDetails() {
|
toggleDetails: function () {
|
||||||
this.showDetails = !this.showDetails;
|
this.updateHeight();
|
||||||
|
this.detailsOpen = !this.detailsOpen;
|
||||||
|
},
|
||||||
|
updateHeight: function () {
|
||||||
|
this.detailsCompleteHeight = this.$refs.detailsInner.offsetHeight;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.energyflow {
|
.energyflow {
|
||||||
cursor: pointer;
|
background: var(--bs-white);
|
||||||
|
}
|
||||||
|
.indicator {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50%) scaleY(1);
|
||||||
|
transition: opacity, transform;
|
||||||
|
transition-duration: var(--evcc-transition-slow);
|
||||||
|
}
|
||||||
|
.energyflow--open .indicator {
|
||||||
|
transform: translateX(-50%) scaleY(-1);
|
||||||
|
}
|
||||||
|
@media (hover: hover) and (pointer: fine) {
|
||||||
|
.energyflow:hover .indicator {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.details {
|
||||||
|
height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
transition: height;
|
||||||
|
transition-duration: var(--evcc-transition-medium);
|
||||||
|
transition-timing-function: cubic-bezier(0.5, 0.5, 0.5, 1.15);
|
||||||
}
|
}
|
||||||
.color-grid {
|
.color-grid {
|
||||||
color: var(--evcc-grid);
|
color: var(--evcc-grid);
|
||||||
|
|
@ -183,11 +237,4 @@ export default {
|
||||||
.color-export {
|
.color-export {
|
||||||
color: var(--evcc-export);
|
color: var(--evcc-export);
|
||||||
}
|
}
|
||||||
.details-icon {
|
|
||||||
text-align: center;
|
|
||||||
width: 30px;
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
55
assets/js/components/Energyflow/EnergyflowEntry.vue
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<div class="d-flex justify-content-between mb-2 entry" :class="{ 'text-gray-medium': !active }">
|
||||||
|
<span class="d-flex flex-nowrap">
|
||||||
|
<BatteryIcon v-if="isBattery" :soc="soc" />
|
||||||
|
<component :is="`shopicon-regular-${icon}`" v-else></component>
|
||||||
|
</span>
|
||||||
|
<span class="text-nowrap flex-grow-1 ms-3">{{ name }}</span>
|
||||||
|
<span class="text-end text-nowrap ps-1 fw-bold"
|
||||||
|
><span v-if="hasSoC">{{ soc }}% / </span>{{ kw(power) }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import "@h2d2/shopicons/es/regular/powersupply";
|
||||||
|
import "@h2d2/shopicons/es/regular/sun";
|
||||||
|
import "@h2d2/shopicons/es/regular/home";
|
||||||
|
import "@h2d2/shopicons/es/regular/car3";
|
||||||
|
import BatteryIcon from "./BatteryIcon.vue";
|
||||||
|
import formatter from "../../mixins/formatter";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "EnergyflowEntry",
|
||||||
|
components: { BatteryIcon },
|
||||||
|
mixins: [formatter],
|
||||||
|
props: {
|
||||||
|
name: { type: String },
|
||||||
|
icon: { type: String },
|
||||||
|
power: { type: Number },
|
||||||
|
soc: { type: Number },
|
||||||
|
valuesInKw: { type: Boolean },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
active: function () {
|
||||||
|
return this.power > 10;
|
||||||
|
},
|
||||||
|
isBattery: function () {
|
||||||
|
return this.icon === "battery";
|
||||||
|
},
|
||||||
|
hasSoC: function () {
|
||||||
|
return this.isBattery && !isNaN(this.soc);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
kw: function (watt) {
|
||||||
|
return this.fmtKw(watt, this.valuesInKw);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.entry {
|
||||||
|
transition: color var(--evcc-transition-medium) linear;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
<template>
|
|
||||||
<div :class="{ 'power-import': this.import, 'power-export': this.export }">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xml:space="preserve"
|
|
||||||
class="grid svg-inline--fa fa-home fa-w-18"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M217.529 46.629H83.817c-8.83 0-16 7.169-16 16v32c0 .196.004.391.011.586v51.22c0 6.209 5.041 11.25 11.25 11.25h22.5c6.209 0 11.25-5.041 11.25-11.25v-35.806h104.701v84.697H83.577c-8.83 0-16 7.169-16 16v32c0 .196.004.391.011.586v51.22c0 6.209 5.041 11.25 11.25 11.25h22.5c6.209 0 11.25-5.041 11.25-11.25v-35.806h104.941v49.925L176.47 506.932l160 .033-39.179-197.538v-50.101h100.402v35.756c0 6.257 5.08 11.337 11.337 11.337h22.674c6.257 0 11.337-5.08 11.337-11.337v-83.756c0-8.831-7.169-16-16-16h-129.75v-84.697h100.642v35.756c0 6.257 5.08 11.337 11.337 11.337h22.674c6.257 0 11.337-5.08 11.337-11.337V62.629c0-8.831-7.169-16-16-16h-129.99V26.162c-.043-12.777-40.058-18.846-40.159-18.633 0 0-39.66 6.092-39.603 18.633v20.467Z"
|
|
||||||
/></svg
|
|
||||||
><fa-icon class="arrow" icon="angle-double-right"></fa-icon>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: "GridIcon",
|
|
||||||
props: {
|
|
||||||
import: { type: Boolean },
|
|
||||||
export: { type: Boolean },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.grid {
|
|
||||||
transform: translateX(0.35rem);
|
|
||||||
transition-property: transform;
|
|
||||||
transition-duration: 250ms;
|
|
||||||
transition-timing-function: ease;
|
|
||||||
}
|
|
||||||
.power-export .grid {
|
|
||||||
transform: translateX(0.9rem);
|
|
||||||
}
|
|
||||||
.power-import .grid {
|
|
||||||
transform: translateX(-0.1rem);
|
|
||||||
}
|
|
||||||
.arrow {
|
|
||||||
margin-left: -0.2rem;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-0.5rem);
|
|
||||||
transition-property: opacity, transform;
|
|
||||||
transition-duration: 250ms;
|
|
||||||
transition-timing-function: ease;
|
|
||||||
}
|
|
||||||
.power-export .arrow {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(-0.9rem) scaleX(1);
|
|
||||||
}
|
|
||||||
.power-import .arrow {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0.1em) scaleX(1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -35,8 +35,7 @@ export default {
|
||||||
.label-bar {
|
.label-bar {
|
||||||
width: 0;
|
width: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 1.7rem;
|
padding: 10px 0;
|
||||||
padding: 0.6rem 0;
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -44,51 +43,50 @@ export default {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.label-bar-scale {
|
.label-bar-scale {
|
||||||
border: 1px solid var(--bs-gray);
|
border: 1px solid var(--bs-gray-medium);
|
||||||
height: 0.5rem;
|
height: 14px;
|
||||||
background: none;
|
background: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
transition: border-radius 500ms linear;
|
transition: border-radius var(--evcc-transition-medium) linear;
|
||||||
}
|
}
|
||||||
.label-bar--top .label-bar-scale {
|
.label-bar--top .label-bar-scale {
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
.label-bar--bottom .label-bar-scale {
|
.label-bar--bottom .label-bar-scale {
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
.label-bar--top.label-bar--first .label-bar-scale {
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
}
|
|
||||||
.label-bar--top.label-bar--last .label-bar-scale {
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
}
|
|
||||||
.label-bar--bottom.label-bar--first .label-bar-scale {
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
}
|
|
||||||
.label-bar--bottom.label-bar--last .label-bar-scale {
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
}
|
|
||||||
.label-bar:not(.label-bar--first) .label-bar-scale {
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
.label-bar-icon {
|
.label-bar-icon {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: var(--bs-gray);
|
transform: scale(1);
|
||||||
padding: 0 0.3rem;
|
color: var(--bs-gray-dark);
|
||||||
opacity: 1;
|
border-radius: 0;
|
||||||
|
border: 0.25rem solid white;
|
||||||
|
transition-property: background-color, transform, border-radius, border;
|
||||||
|
/* will be overwritten by parent component to avoid initial transition */
|
||||||
|
transition-duration: 0s;
|
||||||
|
transition-delay: 0s;
|
||||||
|
transition-timing-function: linear;
|
||||||
}
|
}
|
||||||
.label-bar--top .label-bar-icon {
|
.label-bar--top .label-bar-icon {
|
||||||
margin-top: -6px;
|
margin-top: -12px;
|
||||||
}
|
}
|
||||||
.label-bar--bottom .label-bar-icon {
|
.label-bar--bottom .label-bar-icon {
|
||||||
margin-top: 6px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
.label-bar--hide-icon .label-bar-icon {
|
.label-bar--hide-icon .label-bar-icon {
|
||||||
opacity: 0;
|
background-color: var(--bs-gray-dark);
|
||||||
|
transform: scale(0.1666666);
|
||||||
|
border-radius: 100%;
|
||||||
|
border-width: 1.5rem;
|
||||||
|
transition-delay: 400ms, 0s;
|
||||||
}
|
}
|
||||||
.label-bar--hidden {
|
.label-bar--hidden {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="visualization" :class="{ 'visualization--ready': visualizationReady }">
|
<div class="visualization" :class="{ 'visualization--ready': visualizationReady }">
|
||||||
<div class="label-scale">
|
<div class="label-scale d-flex">
|
||||||
<div class="d-flex justify-content-start">
|
<div class="d-flex justify-content-start flex-grow-1">
|
||||||
<LabelBar v-bind="labelBarProps('top', 'pvProduction')">
|
<LabelBar v-bind="labelBarProps('top', 'pvProduction')">
|
||||||
<fa-icon icon="sun"></fa-icon>
|
<shopicon-regular-sun></shopicon-regular-sun>
|
||||||
</LabelBar>
|
</LabelBar>
|
||||||
<LabelBar v-bind="labelBarProps('top', 'batteryDischarge')">
|
<LabelBar v-bind="labelBarProps('top', 'batteryDischarge')">
|
||||||
<BatteryIcon :soc="batterySoC" discharge />
|
<BatteryIcon :soc="batterySoC" />
|
||||||
</LabelBar>
|
</LabelBar>
|
||||||
<LabelBar v-bind="labelBarProps('top', 'gridImport')">
|
<LabelBar v-bind="labelBarProps('top', 'gridImport')">
|
||||||
<GridIcon import />
|
<shopicon-regular-powersupply></shopicon-regular-powersupply>
|
||||||
</LabelBar>
|
</LabelBar>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="label-scale-name">In</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="site_progress" class="site-progress">
|
<div ref="site_progress" class="site-progress">
|
||||||
<div
|
<div
|
||||||
|
|
@ -51,38 +52,39 @@
|
||||||
<span>{{ $t("main.energyflow.noEnergy") }}</span>
|
<span>{{ $t("main.energyflow.noEnergy") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="label-scale">
|
<div class="label-scale d-flex">
|
||||||
<div class="d-flex justify-content-start">
|
<div class="d-flex justify-content-start flex-grow-1">
|
||||||
<LabelBar v-bind="labelBarProps('bottom', 'homePower')">
|
<LabelBar v-bind="labelBarProps('bottom', 'homePower')">
|
||||||
<fa-icon icon="home"></fa-icon>
|
<shopicon-regular-home></shopicon-regular-home>
|
||||||
</LabelBar>
|
</LabelBar>
|
||||||
<LabelBar v-bind="labelBarProps('bottom', 'loadpoints')">
|
<LabelBar v-bind="labelBarProps('bottom', 'loadpoints')">
|
||||||
<fa-icon icon="car"></fa-icon>
|
<shopicon-regular-car3></shopicon-regular-car3>
|
||||||
</LabelBar>
|
</LabelBar>
|
||||||
<LabelBar v-bind="labelBarProps('bottom', 'batteryCharge')">
|
<LabelBar v-bind="labelBarProps('bottom', 'batteryCharge')">
|
||||||
<BatteryIcon :soc="batterySoC" charge />
|
<BatteryIcon :soc="batterySoC" />
|
||||||
</LabelBar>
|
</LabelBar>
|
||||||
<LabelBar v-bind="labelBarProps('bottom', 'gridExport')">
|
<LabelBar v-bind="labelBarProps('bottom', 'gridExport')">
|
||||||
<GridIcon export />
|
<shopicon-regular-powersupply></shopicon-regular-powersupply>
|
||||||
</LabelBar>
|
</LabelBar>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="label-scale-name">Out</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import "../../icons";
|
|
||||||
import formatter from "../../mixins/formatter";
|
import formatter from "../../mixins/formatter";
|
||||||
import BatteryIcon from "./BatteryIcon.vue";
|
import BatteryIcon from "./BatteryIcon.vue";
|
||||||
import GridIcon from "./GridIcon.vue";
|
|
||||||
import LabelBar from "./LabelBar.vue";
|
import LabelBar from "./LabelBar.vue";
|
||||||
|
import "@h2d2/shopicons/es/regular/car3";
|
||||||
|
import "@h2d2/shopicons/es/regular/sun";
|
||||||
|
import "@h2d2/shopicons/es/regular/home";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Visualization",
|
name: "Visualization",
|
||||||
components: { BatteryIcon, LabelBar, GridIcon },
|
components: { BatteryIcon, LabelBar },
|
||||||
mixins: [formatter],
|
mixins: [formatter],
|
||||||
props: {
|
props: {
|
||||||
showDetails: Boolean,
|
|
||||||
gridImport: { type: Number, default: 0 },
|
gridImport: { type: Number, default: 0 },
|
||||||
selfConsumption: { type: Number, default: 0 },
|
selfConsumption: { type: Number, default: 0 },
|
||||||
pvExport: { type: Number, default: 0 },
|
pvExport: { type: Number, default: 0 },
|
||||||
|
|
@ -99,7 +101,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
gridExport: function () {
|
gridExport: function () {
|
||||||
return this.pvExport;
|
return this.applyThreshold(this.pvExport);
|
||||||
},
|
},
|
||||||
totalRaw: function () {
|
totalRaw: function () {
|
||||||
return this.gridImport + this.selfConsumption + this.pvExport;
|
return this.gridImport + this.selfConsumption + this.pvExport;
|
||||||
|
|
@ -118,9 +120,6 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showDetails: function () {
|
|
||||||
this.$nextTick(() => this.updateElementWidth());
|
|
||||||
},
|
|
||||||
totalAdjusted: function () {
|
totalAdjusted: function () {
|
||||||
if (!this.visualizationReady && this.totalAdjusted > 0)
|
if (!this.visualizationReady && this.totalAdjusted > 0)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -134,7 +133,7 @@ export default {
|
||||||
this.updateElementWidth();
|
this.updateElementWidth();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
window.removeEventListener("resize", this.updateElementWidth);
|
window.removeEventListener("resize", this.updateElementWidth);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -171,38 +170,13 @@ export default {
|
||||||
updateElementWidth() {
|
updateElementWidth() {
|
||||||
this.width = this.$refs.site_progress.getBoundingClientRect().width;
|
this.width = this.$refs.site_progress.getBoundingClientRect().width;
|
||||||
},
|
},
|
||||||
isLabelFirst(position, name) {
|
|
||||||
return this.isLabel(position, name, false);
|
|
||||||
},
|
|
||||||
isLabelLast(position, name) {
|
|
||||||
return this.isLabel(position, name, true);
|
|
||||||
},
|
|
||||||
isLabel(position, name, last) {
|
|
||||||
const labels = {
|
|
||||||
top: ["pvProduction", "batteryDischarge", "gridImport"],
|
|
||||||
bottom: ["homePower", "loadpoints", "batteryCharge", "gridExport"],
|
|
||||||
};
|
|
||||||
const entries = [...labels[position]];
|
|
||||||
if (last) {
|
|
||||||
entries.reverse();
|
|
||||||
}
|
|
||||||
for (let i = 0; i < entries.length; i++) {
|
|
||||||
const entry = entries[i];
|
|
||||||
if (this[entry] > 0) {
|
|
||||||
return entry === name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
labelBarProps(position, name) {
|
labelBarProps(position, name) {
|
||||||
const value = this[name];
|
const value = this[name];
|
||||||
const minWidth = name.startsWith("battery") || name.startsWith("grid") ? 44 : 32;
|
const minWidth = 40;
|
||||||
return {
|
return {
|
||||||
value,
|
value,
|
||||||
hideIcon: this.hideLabelIcon(value, minWidth),
|
hideIcon: this.hideLabelIcon(value, minWidth),
|
||||||
style: { width: this.widthTotal(value) },
|
style: { width: this.widthTotal(value) },
|
||||||
first: this.isLabelFirst(position, name),
|
|
||||||
last: this.isLabelLast(position, name),
|
|
||||||
[position]: true,
|
[position]: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -211,12 +185,21 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.site-progress {
|
.site-progress {
|
||||||
--height: 38px;
|
--height: 2.5rem;
|
||||||
height: var(--height);
|
height: var(--height);
|
||||||
margin: 0.25rem 0;
|
border-radius: 10px;
|
||||||
border-radius: 5px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
margin-right: 1.2rem;
|
||||||
|
}
|
||||||
|
.label-scale-name {
|
||||||
|
color: var(--bs-gray-medium);
|
||||||
|
flex-basis: 1.2rem;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
writing-mode: tb-rl;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.site-progress-bar {
|
.site-progress-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -228,7 +211,7 @@ export default {
|
||||||
}
|
}
|
||||||
.visualization--ready .site-progress-bar {
|
.visualization--ready .site-progress-bar {
|
||||||
transition-property: width;
|
transition-property: width;
|
||||||
transition-duration: 500ms;
|
transition-duration: var(--evcc-transition-medium);
|
||||||
transition-timing-function: linear;
|
transition-timing-function: linear;
|
||||||
}
|
}
|
||||||
.grid-import {
|
.grid-import {
|
||||||
|
|
@ -249,12 +232,12 @@ export default {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.visualization--ready >>> .label-bar {
|
.visualization--ready ::v-deep(.label-bar) {
|
||||||
transition-property: width, opacity;
|
transition-property: width, opacity;
|
||||||
transition-duration: 500ms, 250ms;
|
transition-duration: var(--evcc-transition-medium), var(--evcc-transition-fast);
|
||||||
transition-timing-function: linear, ease;
|
transition-timing-function: linear, ease;
|
||||||
}
|
}
|
||||||
.visualization--ready >>> .label-bar-icon {
|
.visualization--ready ::v-deep(.label-bar-icon) {
|
||||||
transition: opacity 250ms ease-in;
|
transition-duration: var(--evcc-transition-very-fast), 500ms;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import Footer from "./Footer.vue";
|
import Footer from "./Footer.vue";
|
||||||
import i18n from "../i18n";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Main/Footer",
|
title: "Main/Footer",
|
||||||
|
|
@ -7,11 +6,12 @@ export default {
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Template = (args, { argTypes }) => ({
|
const Template = (args) => ({
|
||||||
i18n,
|
setup() {
|
||||||
props: Object.keys(argTypes),
|
return { args };
|
||||||
|
},
|
||||||
components: { Footer },
|
components: { Footer },
|
||||||
template: '<Footer v-bind="$props"></Footer>',
|
template: '<Footer v-bind="args"></Footer>',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const KeinUpdate = Template.bind({});
|
export const KeinUpdate = Template.bind({});
|
||||||
|
|
@ -84,18 +84,3 @@ Upgrade.args = {
|
||||||
selfConsumptionPercent: 0,
|
selfConsumptionPercent: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Savings = Template.bind({});
|
|
||||||
Savings.args = {
|
|
||||||
version: {
|
|
||||||
installed: "0.36",
|
|
||||||
available: "0.40",
|
|
||||||
hasUpdater: true,
|
|
||||||
},
|
|
||||||
savings: {
|
|
||||||
since: 82800,
|
|
||||||
totalCharged: 15231,
|
|
||||||
selfConsumptionCharged: 12231,
|
|
||||||
selfConsumptionPercent: 80.3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<footer class="container">
|
<footer class="footer">
|
||||||
<div class="py-3 py-md-5 mt-3 mt-md-5 border-top">
|
<div class="container py-3">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<Version v-bind="version" />
|
<Version v-bind="version" />
|
||||||
|
<!--
|
||||||
|
<button
|
||||||
|
class="btn btn-link p-0 flex-shrink-0"
|
||||||
|
:class="{ 'text-evcc': $hiddenFeatures, 'text-muted': !$hiddenFeatures }"
|
||||||
|
@click="toggleHiddenFeatures"
|
||||||
|
>
|
||||||
|
<shopicon-filled-testtube size="s"></shopicon-filled-testtube>
|
||||||
|
</button>-->
|
||||||
<!-- Please don't mess with the sponsor status. You risk loosing your mojo. -->
|
<!-- Please don't mess with the sponsor status. You risk loosing your mojo. -->
|
||||||
<Savings v-bind="savings" :sponsor="sponsor" />
|
<Savings v-bind="savings" :sponsor="sponsor" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -11,8 +19,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Version from "./Version";
|
import "@h2d2/shopicons/es/filled/testtube";
|
||||||
import Savings from "./Savings";
|
|
||||||
|
import Version from "./Version.vue";
|
||||||
|
import Savings from "./Savings.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Footer",
|
name: "Footer",
|
||||||
|
|
@ -22,5 +32,22 @@ export default {
|
||||||
sponsor: String,
|
sponsor: String,
|
||||||
savings: Object,
|
savings: Object,
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
toggleHiddenFeatures() {
|
||||||
|
const dialog = `🧪 Experimentelle UI-Elemente ${
|
||||||
|
this.$hiddenFeatures ? "deaktivieren" : "aktivieren"
|
||||||
|
}`;
|
||||||
|
if (window.confirm(dialog)) {
|
||||||
|
window.localStorage["hidden_features"] = !this.$hiddenFeatures;
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer {
|
||||||
|
color: var(--bs-white);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
41
assets/js/components/LabelAndValue.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<div class="root">
|
||||||
|
<div class="mb-2 label">{{ label }}</div>
|
||||||
|
<slot>
|
||||||
|
<h3 class="value m-0">
|
||||||
|
{{ value }}
|
||||||
|
<span v-if="extraValue" class="extraValue d-block d-sm-inline text-nowrap">
|
||||||
|
{{ extraValue }}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "LabelAndValue",
|
||||||
|
props: {
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
extraValue: String,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.root {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--bs-gray-medium);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.extraValue {
|
||||||
|
color: var(--bs-gray-medium);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import Loadpoint from "./Loadpoint.vue";
|
import Loadpoint from "./Loadpoint.vue";
|
||||||
import i18n from "../i18n";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Main/Loadpoint",
|
title: "Main/Loadpoint",
|
||||||
|
|
@ -11,11 +10,12 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Template = (args, { argTypes }) => ({
|
const Template = (args) => ({
|
||||||
i18n,
|
setup() {
|
||||||
props: Object.keys(argTypes),
|
return { args };
|
||||||
|
},
|
||||||
components: { Loadpoint },
|
components: { Loadpoint },
|
||||||
template: '<Loadpoint v-bind="$props"></Loadpoint>',
|
template: '<Loadpoint v-bind="args"></Loadpoint>',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Base = Template.bind({});
|
export const Base = Template.bind({});
|
||||||
|
|
@ -29,9 +29,29 @@ Base.args = {
|
||||||
vehicleTitle: "Mein Auto",
|
vehicleTitle: "Mein Auto",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
connected: true,
|
connected: true,
|
||||||
|
mode: "pv",
|
||||||
charging: true,
|
charging: true,
|
||||||
vehicleSoC: 66,
|
vehicleSoC: 66,
|
||||||
targetSoC: 90,
|
targetSoC: 90,
|
||||||
|
chargeCurrent: 7,
|
||||||
|
minCurrent: 6,
|
||||||
|
maxCurrent: 16,
|
||||||
|
activePhases: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Idle = Template.bind({});
|
||||||
|
Idle.args = {
|
||||||
|
id: 0,
|
||||||
|
chargePower: 0,
|
||||||
|
vehiclePresent: false,
|
||||||
|
enabled: false,
|
||||||
|
connected: false,
|
||||||
|
mode: "off",
|
||||||
|
charging: false,
|
||||||
|
chargeCurrent: 0,
|
||||||
|
minCurrent: 6,
|
||||||
|
maxCurrent: 16,
|
||||||
|
activePhases: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Disabled = Template.bind({});
|
export const Disabled = Template.bind({});
|
||||||
|
|
@ -42,9 +62,16 @@ Disabled.args = {
|
||||||
remoteDisabledSource: "Sunny Home Manager",
|
remoteDisabledSource: "Sunny Home Manager",
|
||||||
vehiclePresent: true,
|
vehiclePresent: true,
|
||||||
vehicleTitle: "Mein Auto",
|
vehicleTitle: "Mein Auto",
|
||||||
|
chargedEnergy: 31211,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
mode: "now",
|
||||||
connected: true,
|
connected: true,
|
||||||
charging: false,
|
charging: false,
|
||||||
|
chargePower: 8112,
|
||||||
vehicleSoC: 66,
|
vehicleSoC: 66,
|
||||||
targetSoC: 100,
|
targetSoC: 100,
|
||||||
|
chargeCurrent: 7,
|
||||||
|
minCurrent: 6,
|
||||||
|
maxCurrent: 16,
|
||||||
|
activePhases: 3,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,92 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="loadpoint bg-white p-4">
|
||||||
<p class="h3 mb-4 d-sm-block" :class="{ 'd-none': single }">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
{{ title || $t("main.loadpoint.fallbackName") }}
|
<h3 class="mb-3 me-2 text-truncate">
|
||||||
</p>
|
{{ title || $t("main.loadpoint.fallbackName") }}
|
||||||
<div v-if="remoteDisabled == 'soft'" class="alert alert-warning mt-4 mb-2" role="alert">
|
</h3>
|
||||||
{{ $t("main.loadpoint.remoteDisabledSoft", { source: remoteDisabledSource }) }}
|
<div class="mb-3 d-flex align-items-center">
|
||||||
</div>
|
<Mode :mode="mode" @updated="setTargetMode" />
|
||||||
<div v-if="remoteDisabled == 'hard'" class="alert alert-danger mt-4 mb-2" role="alert">
|
<button v-if="$hiddenFeatures" class="btn btn-link text-gray p-0 flex-shrink-0">
|
||||||
{{ $t("main.loadpoint.remoteDisabledHard", { source: remoteDisabledSource }) }}
|
<shopicon-filled-options size="s"></shopicon-filled-options>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div
|
||||||
<Mode class="col-12 col-md-6 col-lg-4 mb-4" :mode="mode" @updated="setTargetMode" />
|
v-if="remoteDisabled"
|
||||||
<Vehicle
|
class="alert alert-warning my-4 py-2"
|
||||||
class="col-12 col-md-6 col-lg-8 mb-4"
|
:class="`${remoteDisabled === 'hard' ? 'alert-danger' : 'alert-warning'}`"
|
||||||
v-bind="vehicle"
|
role="alert"
|
||||||
@target-soc-updated="setTargetSoC"
|
>
|
||||||
@target-time-updated="setTargetTime"
|
{{
|
||||||
@target-time-removed="removeTargetTime"
|
$t(
|
||||||
|
remoteDisabled === "hard"
|
||||||
|
? "main.loadpoint.remoteDisabledHard"
|
||||||
|
: "main.loadpoint.remoteDisabledSoft",
|
||||||
|
{ source: remoteDisabledSource }
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details d-flex align-items-start mb-3">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div>
|
||||||
|
<LabelAndValue
|
||||||
|
:label="$t('main.loadpoint.power')"
|
||||||
|
:value="fmtKw(chargePower)"
|
||||||
|
class="mb-2"
|
||||||
|
/>
|
||||||
|
<Phases
|
||||||
|
v-bind="phasesProps"
|
||||||
|
class="opacity-transiton"
|
||||||
|
:class="`opacity-${charging ? '100' : '0'}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<shopicon-regular-lightning
|
||||||
|
class="text-evcc opacity-transiton"
|
||||||
|
:class="`opacity-${charging ? '100' : '0'}`"
|
||||||
|
size="m"
|
||||||
|
></shopicon-regular-lightning>
|
||||||
|
</div>
|
||||||
|
<LabelAndValue :label="$t('main.loadpoint.charged')" :value="fmtKWh(chargedEnergy)" />
|
||||||
|
<LabelAndValue
|
||||||
|
v-if="chargeRemainingDurationInterpolated"
|
||||||
|
:label="$t('main.loadpoint.remaining')"
|
||||||
|
:value="`
|
||||||
|
${fmtShortDuration(chargeRemainingDurationInterpolated)}
|
||||||
|
${fmtShortDurationUnit(chargeRemainingDurationInterpolated, true)}`"
|
||||||
|
/>
|
||||||
|
<LabelAndValue
|
||||||
|
v-else
|
||||||
|
:label="$t('main.loadpoint.duration')"
|
||||||
|
:value="`
|
||||||
|
${fmtShortDuration(chargeDurationInterpolated)}
|
||||||
|
${fmtShortDurationUnit(chargeDurationInterpolated)}`"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<LoadpointDetails v-bind="details" />
|
<Vehicle
|
||||||
|
v-bind="vehicle"
|
||||||
|
@target-soc-updated="setTargetSoC"
|
||||||
|
@target-time-updated="setTargetTime"
|
||||||
|
@target-time-removed="removeTargetTime"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import "@h2d2/shopicons/es/regular/lightning";
|
||||||
|
import "@h2d2/shopicons/es/filled/options";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import Mode from "./Mode";
|
import Mode from "./Mode.vue";
|
||||||
import Vehicle from "./Vehicle";
|
import Vehicle from "./Vehicle.vue";
|
||||||
import LoadpointDetails from "./LoadpointDetails";
|
import Phases from "./Phases.vue";
|
||||||
|
import LabelAndValue from "./LabelAndValue.vue";
|
||||||
import formatter from "../mixins/formatter";
|
import formatter from "../mixins/formatter";
|
||||||
import collector from "../mixins/collector";
|
import collector from "../mixins/collector";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Loadpoint",
|
name: "Loadpoint",
|
||||||
components: { LoadpointDetails, Mode, Vehicle },
|
components: { Mode, Vehicle, Phases, LabelAndValue },
|
||||||
mixins: [formatter, collector],
|
mixins: [formatter, collector],
|
||||||
props: {
|
props: {
|
||||||
id: Number,
|
id: Number,
|
||||||
|
|
@ -60,6 +112,7 @@ export default {
|
||||||
minSoC: Number,
|
minSoC: Number,
|
||||||
targetTime: String,
|
targetTime: String,
|
||||||
targetTimeActive: Boolean,
|
targetTimeActive: Boolean,
|
||||||
|
targetTimeProjectedStart: String,
|
||||||
vehicleProviderLoggedIn: Boolean,
|
vehicleProviderLoggedIn: Boolean,
|
||||||
vehicleProviderLoginPath: String,
|
vehicleProviderLoginPath: String,
|
||||||
vehicleProviderLogoutPath: String,
|
vehicleProviderLogoutPath: String,
|
||||||
|
|
@ -87,15 +140,58 @@ export default {
|
||||||
pvRemaining: Number,
|
pvRemaining: Number,
|
||||||
pvAction: String,
|
pvAction: String,
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tickerHandler: null,
|
||||||
|
phaseRemainingInterpolated: this.phaseRemaining,
|
||||||
|
pvRemainingInterpolated: this.pvRemaining,
|
||||||
|
chargeDurationInterpolated: this.chargeDuration,
|
||||||
|
chargeRemainingDurationInterpolated: this.chargeRemainingDuration,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
details: function () {
|
phasesProps: function () {
|
||||||
return this.collectProps(LoadpointDetails);
|
return this.collectProps(Phases);
|
||||||
},
|
},
|
||||||
vehicle: function () {
|
vehicle: function () {
|
||||||
return this.collectProps(Vehicle);
|
return this.collectProps(Vehicle);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
phaseRemaining() {
|
||||||
|
this.phaseRemainingInterpolated = this.phaseRemaining;
|
||||||
|
},
|
||||||
|
pvRemaining() {
|
||||||
|
this.pvRemainingInterpolated = this.pvRemaining;
|
||||||
|
},
|
||||||
|
chargeDuration() {
|
||||||
|
this.chargeDurationInterpolated = this.chargeDuration;
|
||||||
|
},
|
||||||
|
chargeRemainingDuration() {
|
||||||
|
this.chargeRemainingDurationInterpolated = this.chargeRemainingDuration;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.tickerHandler = setInterval(this.tick, 1000);
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
clearInterval(this.tickerHandler);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
tick() {
|
||||||
|
if (this.phaseRemainingInterpolated > 0) {
|
||||||
|
this.phaseRemainingInterpolated--;
|
||||||
|
}
|
||||||
|
if (this.pvRemainingInterpolated > 0) {
|
||||||
|
this.pvRemainingInterpolated--;
|
||||||
|
}
|
||||||
|
if (this.chargeDurationInterpolated > 0 && this.charging) {
|
||||||
|
this.chargeDurationInterpolated++;
|
||||||
|
}
|
||||||
|
if (this.chargeRemainingDurationInterpolated > 0 && this.charging) {
|
||||||
|
this.chargeRemainingDurationInterpolated--;
|
||||||
|
}
|
||||||
|
},
|
||||||
apiPath: function (func) {
|
apiPath: function (func) {
|
||||||
return "loadpoints/" + this.id + "/" + func;
|
return "loadpoints/" + this.id + "/" + func;
|
||||||
},
|
},
|
||||||
|
|
@ -115,3 +211,24 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loadpoint {
|
||||||
|
border-radius: 2rem;
|
||||||
|
color: var(--bs-gray-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
.details > div:nth-child(2) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.details > div:nth-child(3) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.opacity-transiton {
|
||||||
|
transition: opacity var(--evcc-transition-slow) ease-in;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import LoadpointDetails from "./LoadpointDetails.vue";
|
import LoadpointDetails from "./LoadpointDetails.vue";
|
||||||
import i18n from "../i18n";
|
import i18n from "../i18n";
|
||||||
import "../tooltip";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Main/LoadpointDetails",
|
title: "Main/LoadpointDetails",
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-6 col-sm-3 col-lg-2 mt-3 offset-lg-4">
|
|
||||||
<div class="mb-2 value">
|
|
||||||
{{ $t("main.loadpointDetails.power") }}
|
|
||||||
<div
|
|
||||||
v-if="chargePower && activePhases"
|
|
||||||
v-tooltip="{ content: phaseTooltip }"
|
|
||||||
class="badge rounded-pill bg-secondary text-light cursor-pointer d-inline-flex align-items-center"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<div>{{ activePhases }}P</div>
|
|
||||||
<WaitingDots
|
|
||||||
v-if="phaseTimerVisible"
|
|
||||||
:direction="phaseAction === 'scale1p' ? 'down' : 'up'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<fa-icon
|
|
||||||
v-if="climater == 'heating'"
|
|
||||||
class="text-primary ms-1"
|
|
||||||
icon="temperature-low"
|
|
||||||
></fa-icon>
|
|
||||||
<fa-icon
|
|
||||||
v-if="climater == 'cooling'"
|
|
||||||
class="text-primary ms-1"
|
|
||||||
icon="temperature-high"
|
|
||||||
></fa-icon>
|
|
||||||
<fa-icon
|
|
||||||
v-if="climater == 'on'"
|
|
||||||
class="text-primary ms-1"
|
|
||||||
icon="thermometer-half"
|
|
||||||
></fa-icon>
|
|
||||||
</div>
|
|
||||||
<h3 class="value">
|
|
||||||
{{ fmt(chargePower) }}
|
|
||||||
<small class="text-muted">{{ fmtUnit(chargePower) }}W</small
|
|
||||||
><small
|
|
||||||
v-if="pvTimerVisible"
|
|
||||||
v-tooltip="{
|
|
||||||
content: $t(`main.loadpointDetails.tooltip.pv.${pvAction}`, {
|
|
||||||
remaining: fmtShortDuration(pvRemainingInterpolated, true),
|
|
||||||
}),
|
|
||||||
}"
|
|
||||||
class="text-muted cursor-pointer d-inline-block align-bottom"
|
|
||||||
style="margin-bottom: 0.1em"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<WaitingDots :direction="pvAction === 'disable' ? 'down' : 'up'" />
|
|
||||||
</small>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-6 col-sm-3 col-lg-2 mt-3">
|
|
||||||
<div class="mb-2 value">{{ $t("main.loadpointDetails.charged") }}</div>
|
|
||||||
<h3 class="value">
|
|
||||||
{{ fmt(chargedEnergy) }}
|
|
||||||
<small class="text-muted">{{ fmtUnit(chargedEnergy) }}Wh</small>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="vehicleRange && vehicleRange >= 0" class="col-6 col-sm-3 col-lg-2 mt-3">
|
|
||||||
<div class="mb-2 value">{{ $t("main.loadpointDetails.vehicleRange") }}</div>
|
|
||||||
<h3 class="value">
|
|
||||||
{{ Math.round(vehicleRange) }}
|
|
||||||
<small class="text-muted">km</small>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="col-6 col-sm-3 col-lg-2 mt-3">
|
|
||||||
<div class="mb-2 value">{{ $t("main.loadpointDetails.duration") }}</div>
|
|
||||||
<h3 class="value">
|
|
||||||
{{ fmtShortDuration(chargeDurationInterpolated) }}
|
|
||||||
<small class="text-muted">{{
|
|
||||||
fmtShortDurationUnit(chargeDurationInterpolated)
|
|
||||||
}}</small>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="vehiclePresent" class="col-6 col-sm-3 col-lg-2 mt-3">
|
|
||||||
<div class="mb-2 value">{{ $t("main.loadpointDetails.remaining") }}</div>
|
|
||||||
<h3 class="value">
|
|
||||||
{{ fmtShortDuration(chargeRemainingDurationInterpolated) }}
|
|
||||||
<small class="text-muted">{{
|
|
||||||
fmtShortDurationUnit(chargeRemainingDurationInterpolated, true)
|
|
||||||
}}</small>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import WaitingDots from "./WaitingDots";
|
|
||||||
import formatter from "../mixins/formatter";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "LoadpointDetails",
|
|
||||||
components: { WaitingDots },
|
|
||||||
mixins: [formatter],
|
|
||||||
props: {
|
|
||||||
chargedEnergy: Number,
|
|
||||||
chargeDuration: Number,
|
|
||||||
chargeRemainingDuration: Number,
|
|
||||||
chargePower: Number,
|
|
||||||
climater: String,
|
|
||||||
vehiclePresent: Boolean,
|
|
||||||
vehicleRange: Number,
|
|
||||||
activePhases: Number,
|
|
||||||
phaseRemaining: Number,
|
|
||||||
phaseAction: String,
|
|
||||||
pvRemaining: Number,
|
|
||||||
pvAction: String,
|
|
||||||
charging: Boolean,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
tickerHandler: null,
|
|
||||||
phaseRemainingInterpolated: this.phaseRemaining,
|
|
||||||
pvRemainingInterpolated: this.pvRemaining,
|
|
||||||
chargeDurationInterpolated: this.chargeDuration,
|
|
||||||
chargeRemainingDurationInterpolated: this.chargeRemainingDuration,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
phaseTooltip() {
|
|
||||||
if (["scale1p", "scale3p"].includes(this.phaseAction)) {
|
|
||||||
return this.$t(`main.loadpointDetails.tooltip.phases.${this.phaseAction}`, {
|
|
||||||
remaining: this.fmtShortDuration(this.phaseRemainingInterpolated, true),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.$t(`main.loadpointDetails.tooltip.phases.charge${this.activePhases}p`);
|
|
||||||
},
|
|
||||||
phaseTimerActive() {
|
|
||||||
return (
|
|
||||||
this.phaseRemainingInterpolated > 0 &&
|
|
||||||
["scale1p", "scale3p"].includes(this.phaseAction)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
pvTimerActive() {
|
|
||||||
return (
|
|
||||||
this.pvRemainingInterpolated > 0 && ["enable", "disable"].includes(this.pvAction)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
phaseTimerVisible() {
|
|
||||||
if (this.phaseTimerActive && !this.pvTimerActive) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.phaseTimerActive && this.pvTimerActive) {
|
|
||||||
return this.phaseRemainingInterpolated < this.pvRemainingInterpolated; // only show next timer
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
pvTimerVisible() {
|
|
||||||
if (this.pvTimerActive && !this.phaseTimerActive) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.pvTimerActive && this.phaseTimerActive) {
|
|
||||||
return this.pvRemainingInterpolated < this.phaseRemainingInterpolated; // only show next timer
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
phaseRemaining() {
|
|
||||||
this.phaseRemainingInterpolated = this.phaseRemaining;
|
|
||||||
},
|
|
||||||
pvRemaining() {
|
|
||||||
this.pvRemainingInterpolated = this.pvRemaining;
|
|
||||||
},
|
|
||||||
chargeDuration() {
|
|
||||||
this.chargeDurationInterpolated = this.chargeDuration;
|
|
||||||
},
|
|
||||||
chargeRemainingDuration() {
|
|
||||||
this.chargeRemainingDurationInterpolated = this.chargeRemainingDuration;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.tickerHandler = setInterval(this.tick, 1000);
|
|
||||||
},
|
|
||||||
destroyed() {
|
|
||||||
clearInterval(this.tickerHandler);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
tick() {
|
|
||||||
if (this.phaseRemainingInterpolated > 0) {
|
|
||||||
this.phaseRemainingInterpolated--;
|
|
||||||
}
|
|
||||||
if (this.pvRemainingInterpolated > 0) {
|
|
||||||
this.pvRemainingInterpolated--;
|
|
||||||
}
|
|
||||||
if (this.chargeDurationInterpolated > 0 && this.charging) {
|
|
||||||
this.chargeDurationInterpolated++;
|
|
||||||
}
|
|
||||||
if (this.chargeRemainingDurationInterpolated > 0 && this.charging) {
|
|
||||||
this.chargeRemainingDurationInterpolated--;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
172
assets/js/components/Loadpoints.vue
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="container container--loadpoint px-0 mb-3 mb-md-4 d-flex flex-column justify-content-center"
|
||||||
|
>
|
||||||
|
<div ref="carousel" class="carousel d-lg-flex flex-wrap">
|
||||||
|
<div
|
||||||
|
v-for="(loadpoint, index) in loadpoints"
|
||||||
|
:key="index"
|
||||||
|
class="flex-grow-1 mb-3 m-lg-0 p-lg-0"
|
||||||
|
>
|
||||||
|
<Loadpoint
|
||||||
|
v-bind="loadpoint"
|
||||||
|
:id="index"
|
||||||
|
class="h-100"
|
||||||
|
:class="{ 'loadpoint-unselected': !selected(index) }"
|
||||||
|
@click="scrollTo(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="loadpoints.length > 1" class="d-flex d-lg-none justify-content-center">
|
||||||
|
<button
|
||||||
|
v-for="(loadpoint, index) in loadpoints"
|
||||||
|
:key="index"
|
||||||
|
class="btn btn-sm btn-link p-0 mx-1 indicator d-flex justify-content-center align-items-center text-white"
|
||||||
|
:class="{ 'indicator--selected': selected(index) }"
|
||||||
|
@click="scrollTo(index)"
|
||||||
|
>
|
||||||
|
<shopicon-filled-circle class="indicator-icon"></shopicon-filled-circle>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import "@h2d2/shopicons/es/filled/circle";
|
||||||
|
|
||||||
|
import Loadpoint from "./Loadpoint.vue";
|
||||||
|
import collector from "../mixins/collector";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Site",
|
||||||
|
components: { Loadpoint },
|
||||||
|
mixins: [collector],
|
||||||
|
props: {
|
||||||
|
loadpoints: Array,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { selectedIndex: 0, snapTimeout: null };
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$refs.carousel.addEventListener("scroll", this.handleCarouselScroll, false);
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.$refs.carousel.removeEventListener("scroll", this.handleCarouselScroll);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleCarouselScroll() {
|
||||||
|
const { scrollLeft } = this.$refs.carousel;
|
||||||
|
const { offsetWidth } = this.$refs.carousel.children[0];
|
||||||
|
this.selectedIndex = Math.round((scrollLeft - 7.5) / offsetWidth);
|
||||||
|
},
|
||||||
|
selected(index) {
|
||||||
|
return this.selectedIndex === index;
|
||||||
|
},
|
||||||
|
scrollTo(index) {
|
||||||
|
if (this.selectedIndex === index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedIndex = index;
|
||||||
|
const $carousel = this.$refs.carousel;
|
||||||
|
const width = $carousel.children[0].offsetWidth;
|
||||||
|
$carousel.style.scrollSnapType = "none";
|
||||||
|
$carousel.scrollTo({ top: 0, left: 7.5 + width * index, behavior: "smooth" });
|
||||||
|
|
||||||
|
clearTimeout(this.snapTimeout);
|
||||||
|
this.snapTimeout = setTimeout(() => {
|
||||||
|
this.$refs.carousel.style.scrollSnapType = "x mandatory";
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.container--loadpoint {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.carousel {
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
overflow-x: scroll;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE 10+ */
|
||||||
|
}
|
||||||
|
.carousel::-webkit-scrollbar {
|
||||||
|
display: none; /* Blink, Webkit */
|
||||||
|
}
|
||||||
|
.carousel > * {
|
||||||
|
scroll-snap-align: center;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
.indicator {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
opacity: 0.3;
|
||||||
|
transition: opacity var(--evcc-transition-fast) ease-in;
|
||||||
|
}
|
||||||
|
.indicator--selected {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.indicator-icon {
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
.loadpoint {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
transition-duration: var(--evcc-transition-fast);
|
||||||
|
transition-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
.loadpoint-unselected {
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* show truncated tiles on breakpoind sm,md */
|
||||||
|
@media (min-width: 576px) and (max-width: 991.98px) {
|
||||||
|
.container--loadpoint {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
.carousel > *:first-child {
|
||||||
|
margin-left: calc((100vw - var(--slide-width)) / 2);
|
||||||
|
}
|
||||||
|
.carousel > *:last-child {
|
||||||
|
margin-right: calc((100vw - var(--slide-width)) / 2);
|
||||||
|
}
|
||||||
|
/* fixes safari issue with end-side padding https://webplatform.news/issues/2019-08-07 */
|
||||||
|
.carousel::after {
|
||||||
|
content: "";
|
||||||
|
padding-right: 0.02px;
|
||||||
|
}
|
||||||
|
.carousel > * {
|
||||||
|
min-width: var(--slide-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* breakpoind sm */
|
||||||
|
@media (min-width: 576px) and (max-width: 767.98px) {
|
||||||
|
.carousel {
|
||||||
|
--slide-width: 540px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* breakpoind md */
|
||||||
|
@media (min-width: 768px) and (max-width: 991.98px) {
|
||||||
|
.carousel {
|
||||||
|
--slide-width: 720px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* breakpoind lg */
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.carousel {
|
||||||
|
display: grid !important;
|
||||||
|
grid-gap: 2rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,27 +1,20 @@
|
||||||
import Mode from "./Mode.vue";
|
import Mode from "./Mode.vue";
|
||||||
import i18n from "../i18n";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Main/Mode",
|
title: "Main/Mode",
|
||||||
component: Mode,
|
component: Mode,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
mode: { control: { type: "inline-radio", options: ["off", "now", "minpv", "pv"] } },
|
mode: { control: { type: "inline-radio" }, options: ["off", "now", "minpv", "pv"] },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Template = (args, { argTypes }) => ({
|
const Template = (args) => ({
|
||||||
i18n,
|
setup() {
|
||||||
props: Object.keys(argTypes),
|
return { args };
|
||||||
|
},
|
||||||
components: { Mode },
|
components: { Mode },
|
||||||
template: '<Mode v-bind="$props"></Mode>',
|
template: '<Mode v-bind="args"></Mode>',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Base = Template.bind({});
|
export const Base = Template.bind({});
|
||||||
Base.args = {};
|
Base.args = { mode: "pv" };
|
||||||
|
|
||||||
export const CaptionAndPV = Template.bind({});
|
|
||||||
CaptionAndPV.args = {
|
|
||||||
caption: true,
|
|
||||||
pvConfigured: true,
|
|
||||||
mode: "pv",
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="$hiddenFeatures" class="mode-group border d-inline-flex" role="group">
|
||||||
<div class="mb-3">{{ $t("main.mode.title") }}</div>
|
<button
|
||||||
<div class="btn-group w-100" role="group">
|
v-for="m in ['fast', 'cheap']"
|
||||||
<button
|
:key="m"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-secondary"
|
class="btn"
|
||||||
:class="{ active: mode == 'off' }"
|
:class="{ active: isActive(m) }"
|
||||||
@click="setTargetMode('off')"
|
@click="setTargetMode(m)"
|
||||||
>
|
>
|
||||||
{{ $t("main.mode.stop") }}
|
{{ $t(`main.mode.${m}`) }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
</div>
|
||||||
type="button"
|
<div v-else class="mode-group border d-inline-flex" role="group">
|
||||||
class="btn btn-outline-secondary"
|
<button
|
||||||
:class="{ active: mode == 'now' }"
|
v-for="m in modes"
|
||||||
@click="setTargetMode('now')"
|
:key="m"
|
||||||
>
|
type="button"
|
||||||
{{ $t("main.mode.now") }}
|
class="btn"
|
||||||
</button>
|
:class="{ active: isActive(m) }"
|
||||||
<button
|
@click="setTargetMode(m)"
|
||||||
type="button"
|
>
|
||||||
class="btn btn-outline-secondary"
|
<span class="d-inline d-sm-none"> {{ $t(`main.mode.${m}Short`) }} </span>
|
||||||
:class="{ active: mode == 'minpv' }"
|
<span class="d-none d-sm-inline"> {{ $t(`main.mode.${m}Long`) }} </span>
|
||||||
@click="setTargetMode('minpv')"
|
</button>
|
||||||
>
|
|
||||||
<span class="d-inline d-sm-none"> {{ $t("main.mode.minpvShort") }} </span>
|
|
||||||
<span class="d-none d-sm-inline"> {{ $t("main.mode.minpvLong") }} </span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
:class="{ active: mode == 'pv' }"
|
|
||||||
@click="setTargetMode('pv')"
|
|
||||||
>
|
|
||||||
<span class="d-inline d-sm-none"> {{ $t("main.mode.pvShort") }} </span>
|
|
||||||
<span class="d-none d-sm-inline"> {{ $t("main.mode.pvLong") }} </span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -46,18 +32,49 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
mode: String,
|
mode: String,
|
||||||
},
|
},
|
||||||
|
emits: ["updated"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modes: ["off", "now", "minpv", "pv"],
|
||||||
|
};
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isActive: function (mode) {
|
||||||
|
return this.mode === this.mapToOldModes(mode);
|
||||||
|
},
|
||||||
setTargetMode: function (mode) {
|
setTargetMode: function (mode) {
|
||||||
this.$emit("updated", mode);
|
this.$emit("updated", this.mapToOldModes(mode));
|
||||||
|
},
|
||||||
|
mapToOldModes: function (mode) {
|
||||||
|
const mapping = { fast: "now", cheap: "pv" };
|
||||||
|
return mapping[mode] || mode;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.mode-group {
|
||||||
|
border: 2px solid var(--bs-gray-medium);
|
||||||
|
background-color: var(--bs-white);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
/* equal width buttons */
|
/* equal width buttons */
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 0.1em 0.8em;
|
||||||
|
}
|
||||||
|
.btn.active {
|
||||||
|
background: var(--bs-gray-dark);
|
||||||
|
color: var(--bs-white);
|
||||||
|
}
|
||||||
|
.btn-group {
|
||||||
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import Notifications from "./Notifications.vue";
|
import Notifications from "./Notifications.vue";
|
||||||
import i18n from "../i18n";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Main/Notifications",
|
title: "Main/Notifications",
|
||||||
|
|
@ -7,11 +6,12 @@ export default {
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Template = (args, { argTypes }) => ({
|
const Template = (args) => ({
|
||||||
i18n,
|
setup() {
|
||||||
props: Object.keys(argTypes),
|
return { args };
|
||||||
|
},
|
||||||
components: { Notifications },
|
components: { Notifications },
|
||||||
template: '<Notifications v-bind="$props"></Notifications>',
|
template: '<Notifications v-bind="args"></Notifications>',
|
||||||
});
|
});
|
||||||
|
|
||||||
function timeAgo(hours = 0, minutes = 0, seconds = 0) {
|
function timeAgo(hours = 0, minutes = 0, seconds = 0) {
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,20 @@
|
||||||
href="#"
|
href="#"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#notificationModal"
|
data-bs-target="#notificationModal"
|
||||||
class="btn btn-link text-decoration-none link-light text-nowrap"
|
class="btn btn-sm btn-link text-decoration-none link-light text-nowrap"
|
||||||
>
|
>
|
||||||
<fa-icon :class="iconClass" icon="exclamation-triangle"></fa-icon>
|
<shopicon-regular-exclamationtriangle
|
||||||
|
:class="iconClass"
|
||||||
|
></shopicon-regular-exclamationtriangle>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="notificationModal"
|
id="notificationModal"
|
||||||
class="modal fade"
|
class="modal fade text-dark"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
data-bs-backdrop="true"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable"
|
class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable"
|
||||||
|
|
@ -40,14 +43,13 @@
|
||||||
{{ fmtTimeAgo(msg.time - new Date()) }}
|
{{ fmtTimeAgo(msg.time - new Date()) }}
|
||||||
</small>
|
</small>
|
||||||
<p class="d-flex align-items-baseline">
|
<p class="d-flex align-items-baseline">
|
||||||
<fa-icon
|
<shopicon-regular-exclamationtriangle
|
||||||
:class="{
|
:class="{
|
||||||
'text-danger': msg.type === 'error',
|
'text-danger': msg.type === 'error',
|
||||||
'text-warning': msg.type === 'warn',
|
'text-warning': msg.type === 'warn',
|
||||||
}"
|
}"
|
||||||
class="flex-grow-0 d-block"
|
class="flex-grow-0 d-block"
|
||||||
icon="exclamation-triangle"
|
></shopicon-regular-exclamationtriangle>
|
||||||
></fa-icon>
|
|
||||||
<span class="flex-grow-1 px-2 py-1 text-break">
|
<span class="flex-grow-1 px-2 py-1 text-break">
|
||||||
{{ msg.message }}
|
{{ msg.message }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -75,7 +77,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import "../icons";
|
import "@h2d2/shopicons/es/regular/exclamationtriangle";
|
||||||
import formatter from "../mixins/formatter";
|
import formatter from "../mixins/formatter";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -99,7 +101,7 @@ export default {
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
}, 10 * 1000);
|
}, 10 * 1000);
|
||||||
},
|
},
|
||||||
destroyed: function () {
|
unmounted: function () {
|
||||||
clearTimeout(this.interval);
|
clearTimeout(this.interval);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
59
assets/js/components/Phases.stories.js
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import Phases from "./Phases.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Main/Phases",
|
||||||
|
component: Phases,
|
||||||
|
argTypes: {},
|
||||||
|
parameters: {
|
||||||
|
backgrounds: {
|
||||||
|
default: "light",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: { Phases },
|
||||||
|
template: '<Phases v-bind="$props"></Phases>',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Base = Template.bind({});
|
||||||
|
Base.args = {
|
||||||
|
activePhases: 1,
|
||||||
|
minCurrent: 6,
|
||||||
|
maxCurrent: 32,
|
||||||
|
chargeCurrent: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OnePhase = Template.bind({});
|
||||||
|
OnePhase.args = {
|
||||||
|
activePhases: 1,
|
||||||
|
minCurrent: 6,
|
||||||
|
maxCurrent: 16,
|
||||||
|
chargeCurrent: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TwoPhase = Template.bind({});
|
||||||
|
TwoPhase.args = {
|
||||||
|
activePhases: 2,
|
||||||
|
minCurrent: 6,
|
||||||
|
maxCurrent: 16,
|
||||||
|
chargeCurrent: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThreePhases = Template.bind({});
|
||||||
|
ThreePhases.args = {
|
||||||
|
activePhases: 3,
|
||||||
|
minCurrent: 6,
|
||||||
|
maxCurrent: 16,
|
||||||
|
chargeCurrent: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RealCurrents = Template.bind({});
|
||||||
|
RealCurrents.args = {
|
||||||
|
activePhases: 3,
|
||||||
|
minCurrent: 6,
|
||||||
|
maxCurrent: 32,
|
||||||
|
chargeCurrent: 16.5,
|
||||||
|
chargeCurrents: [15.2, 15.76, 14.5],
|
||||||
|
};
|
||||||
99
assets/js/components/Phases.vue
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<div class="phases d-flex justify-content-between">
|
||||||
|
<div
|
||||||
|
v-for="num in [1, 2, 3]"
|
||||||
|
:key="num"
|
||||||
|
class="phase me-1"
|
||||||
|
:class="{ inactive: inactive(num) }"
|
||||||
|
>
|
||||||
|
<div class="target" :style="{ width: `${targetWidth()}%` }"></div>
|
||||||
|
<div class="real" :style="{ width: `${realWidth(num)}%` }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Phases",
|
||||||
|
props: {
|
||||||
|
chargeCurrent: { type: Number },
|
||||||
|
chargeCurrents: { type: Object },
|
||||||
|
activePhases: { type: Number },
|
||||||
|
minCurrent: { type: Number },
|
||||||
|
maxCurrent: { type: Number },
|
||||||
|
phaseAction: { type: String },
|
||||||
|
phaseRemaining: { type: Number },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
phaseTimerVisible() {
|
||||||
|
if (this.phaseTimerActive && !this.pvTimerActive) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.phaseTimerActive && this.pvTimerActive) {
|
||||||
|
return this.phaseRemainingInterpolated < this.pvRemainingInterpolated; // only show next timer
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
pvTimerVisible() {
|
||||||
|
if (this.pvTimerActive && !this.phaseTimerActive) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.pvTimerActive && this.phaseTimerActive) {
|
||||||
|
return this.pvRemainingInterpolated < this.phaseRemainingInterpolated; // only show next timer
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
inactive(num) {
|
||||||
|
return num > this.activePhases;
|
||||||
|
},
|
||||||
|
targetWidth() {
|
||||||
|
let current = Math.min(Math.max(this.minCurrent, this.chargeCurrent), this.maxCurrent);
|
||||||
|
return (100 / this.maxCurrent) * current;
|
||||||
|
},
|
||||||
|
realWidth(num) {
|
||||||
|
if (this.chargeCurrents) {
|
||||||
|
const current = this.chargeCurrents[num - 1] || 0;
|
||||||
|
return (100 / this.maxCurrent) * current;
|
||||||
|
}
|
||||||
|
return this.targetWidth();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.phases {
|
||||||
|
width: 68px;
|
||||||
|
}
|
||||||
|
.phase {
|
||||||
|
background-color: var(--bs-gray-200);
|
||||||
|
height: 4px;
|
||||||
|
width: 20px;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.target,
|
||||||
|
.real {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
transition-property: width, opacity;
|
||||||
|
transition-duration: var(--evcc-transition-slow);
|
||||||
|
transition-timing-function: ease-in;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.target {
|
||||||
|
background-color: var(--evcc-green);
|
||||||
|
}
|
||||||
|
.real {
|
||||||
|
background-color: var(--evcc-dark-green);
|
||||||
|
}
|
||||||
|
.phase.inactive .target,
|
||||||
|
.phase.inactive .real {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
assets/js/components/Savings.stories.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import Savings from "./Savings.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Main/Footer/Savings",
|
||||||
|
component: Savings,
|
||||||
|
argTypes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args) => ({
|
||||||
|
setup() {
|
||||||
|
return { args };
|
||||||
|
},
|
||||||
|
components: { Savings },
|
||||||
|
template: '<div class="d-flex justify-content-end"><Savings v-bind="args"></Savings></div>',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
Default.args = {
|
||||||
|
since: 82800,
|
||||||
|
totalCharged: 15231,
|
||||||
|
selfConsumptionCharged: 12231,
|
||||||
|
selfConsumptionPercent: 80.3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoCharge = Template.bind({});
|
||||||
|
NoCharge.args = {
|
||||||
|
since: 82800,
|
||||||
|
totalCharged: 0,
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-link pe-0 text-decoration-none link-dark text-nowrap"
|
class="btn btn-link pe-0 text-decoration-none text-white text-nowrap d-flex align-items-end"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#savingsModal"
|
data-bs-target="#savingsModal"
|
||||||
>
|
>
|
||||||
|
|
@ -10,154 +10,164 @@
|
||||||
}}</span
|
}}</span
|
||||||
><span class="d-none d-sm-inline">{{
|
><span class="d-none d-sm-inline">{{
|
||||||
$t("footer.savings.footerLong", { percent })
|
$t("footer.savings.footerLong", { percent })
|
||||||
}}</span
|
}}</span>
|
||||||
><fa-icon icon="sun" class="icon ms-2 text-evcc"></fa-icon>
|
<shopicon-regular-sun class="ms-2 text-evcc"></shopicon-regular-sun>
|
||||||
</button>
|
</button>
|
||||||
<div
|
|
||||||
id="savingsModal"
|
<Teleport to="body">
|
||||||
ref="modal"
|
<div
|
||||||
class="modal fade"
|
id="savingsModal"
|
||||||
tabindex="-1"
|
ref="modal"
|
||||||
role="dialog"
|
class="modal fade text-dark"
|
||||||
aria-hidden="true"
|
data-bs-backdrop="true"
|
||||||
>
|
tabindex="-1"
|
||||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
role="dialog"
|
||||||
<div class="modal-content">
|
aria-hidden="true"
|
||||||
<div class="modal-header">
|
>
|
||||||
<h5 class="modal-title">
|
<div
|
||||||
<span class="d-block d-sm-none">
|
class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable"
|
||||||
{{
|
role="document"
|
||||||
$t("footer.savings.modalTitleShort", {
|
>
|
||||||
percent,
|
<div class="modal-content">
|
||||||
total: fmtKw(totalCharged * 1000, true, false),
|
<div class="modal-header">
|
||||||
})
|
<h5 class="modal-title">
|
||||||
}}
|
<span class="text-nowrap">
|
||||||
</span>
|
|
||||||
<span class="d-none d-sm-block">
|
|
||||||
{{
|
|
||||||
$t("footer.savings.modalTitleLong", {
|
|
||||||
percent,
|
|
||||||
total: fmtKw(totalCharged * 1000, true, false),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn-close"
|
|
||||||
data-bs-dismiss="modal"
|
|
||||||
aria-label="Close"
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body py-4">
|
|
||||||
<div class="chart-container mb-3">
|
|
||||||
<div class="chart-legend d-flex flex-wrap justify-content-between mb-1">
|
|
||||||
<div class="text-nowrap">
|
|
||||||
<fa-icon icon="square" class="text-evcc"></fa-icon>
|
|
||||||
{{
|
{{
|
||||||
$t("footer.savings.modalChartSelf", {
|
$t("footer.savings.modalTitlePartOne", {
|
||||||
self: fmtKw(selfConsumptionCharged * 1000, true, false),
|
total: fmtKw(totalCharged * 1000, true, false),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</div>
|
·
|
||||||
<div class="text-nowrap">
|
</span>
|
||||||
<fa-icon icon="square" class="text-grid"></fa-icon>
|
{{ " " }}
|
||||||
{{
|
<span class="text-nowrap">
|
||||||
$t("footer.savings.modalChartGrid", {
|
{{ $t("footer.savings.modalTitlePartTwo", { percent }) }}
|
||||||
grid: fmtKw(gridCharged * 1000, true, false),
|
</span>
|
||||||
})
|
</h5>
|
||||||
}}
|
<button
|
||||||
</div>
|
type="button"
|
||||||
</div>
|
class="btn-close"
|
||||||
<div
|
data-bs-dismiss="modal"
|
||||||
class="chart d-flex justify-content-stretch mb-1 rounded overflow-hidden"
|
aria-label="Close"
|
||||||
>
|
></button>
|
||||||
<div
|
|
||||||
v-if="totalCharged > 0"
|
|
||||||
class="chart-item chart-item--self d-flex justify-content-center text-white flex-shrink-1"
|
|
||||||
:style="{ width: `${percent}%` }"
|
|
||||||
>
|
|
||||||
<span class="text-truncate"> {{ percent }}% </span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="totalCharged > 0"
|
|
||||||
class="chart-item chart-item--grid d-flex justify-content-center text-white flex-shrink-1"
|
|
||||||
:style="{ width: `${100 - percent}%` }"
|
|
||||||
>
|
|
||||||
<span class="text-truncate"> {{ 100 - percent }}% </span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="totalCharged === 0"
|
|
||||||
class="chart-item chart-item--no-data d-flex justify-content-center text-white w-100"
|
|
||||||
>
|
|
||||||
<span>{{ $t("footer.savings.modalNoData") }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-3">
|
<div class="modal-body py-4">
|
||||||
{{ $t("footer.savings.modalSavingsPrice") }}:
|
<div class="chart-container mb-3">
|
||||||
<strong>{{ fmtPricePerKWh(effectivePrice, currency) }}</strong>
|
<div
|
||||||
<br />
|
class="chart-legend d-flex flex-wrap justify-content-between mb-1"
|
||||||
{{ $t("footer.savings.modalSavingsTotal") }}:
|
>
|
||||||
<strong>{{ fmtMoney(amount, currency) }}</strong>
|
<div
|
||||||
</p>
|
class="text-nowrap"
|
||||||
|
:class="{ 'text-gray-medium': noData }"
|
||||||
|
>
|
||||||
|
<shopicon-filled-square
|
||||||
|
class="text-evcc d-inline-block"
|
||||||
|
:class="{ 'text-gray-medium': noData }"
|
||||||
|
></shopicon-filled-square>
|
||||||
|
{{
|
||||||
|
$t("footer.savings.modalChartSelf", {
|
||||||
|
self: fmtKw(
|
||||||
|
selfConsumptionCharged * 1000,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-nowrap"
|
||||||
|
:class="{ 'text-gray-medium': noData }"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t("footer.savings.modalChartGrid", {
|
||||||
|
grid: fmtKw(gridCharged * 1000, true, false),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<shopicon-filled-square
|
||||||
|
class="text-grid d-inline-block"
|
||||||
|
:class="{ 'text-gray-medium': noData }"
|
||||||
|
></shopicon-filled-square>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="chart d-flex justify-content-stretch mb-1 rounded overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!noData"
|
||||||
|
class="chart-item chart-item--self d-flex justify-content-center align-items-center text-white flex-shrink-1"
|
||||||
|
:style="{ width: `${percent}%` }"
|
||||||
|
>
|
||||||
|
<span class="text-truncate"> {{ percent }}% </span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!noData"
|
||||||
|
class="chart-item chart-item--grid d-flex justify-content-center align-items-center text-white flex-shrink-1"
|
||||||
|
:style="{ width: `${100 - percent}%` }"
|
||||||
|
>
|
||||||
|
<span class="text-truncate"> {{ 100 - percent }}% </span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="noData"
|
||||||
|
class="chart-item chart-item--no-data d-flex justify-content-center align-items-center text-white w-100"
|
||||||
|
>
|
||||||
|
<span>{{ $t("footer.savings.modalNoData") }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mb-3">
|
||||||
|
{{ $t("footer.savings.modalSavingsPrice") }}:
|
||||||
|
<strong>{{ fmtPricePerKWh(effectivePrice, currency) }}</strong>
|
||||||
|
<br />
|
||||||
|
{{ $t("footer.savings.modalSavingsTotal") }}:
|
||||||
|
<strong>{{ fmtMoney(amount, currency) }}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p class="small text-muted mb-3">
|
<p class="small text-muted mb-3">
|
||||||
<a
|
<a
|
||||||
href="https://docs.evcc.io/docs/guides/setup/#ersparnisberechnung"
|
href="https://docs.evcc.io/docs/guides/setup/#ersparnisberechnung"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="text-muted"
|
class="small"
|
||||||
>
|
>
|
||||||
{{ $t("footer.savings.modalExplaination") }}</a
|
{{ $t("footer.savings.modalExplaination") }}</a
|
||||||
>:
|
>:
|
||||||
<span class="text-nowrap">
|
<span class="text-nowrap">
|
||||||
|
{{
|
||||||
|
$t("footer.savings.modalExplainationGrid", {
|
||||||
|
gridPrice: fmtPricePerKWh(gridPrice, currency),
|
||||||
|
})
|
||||||
|
}}</span
|
||||||
|
>,
|
||||||
|
<span class="text-nowrap">
|
||||||
|
{{
|
||||||
|
$t("footer.savings.modalExplainationFeedIn", {
|
||||||
|
feedInPrice: fmtPricePerKWh(feedInPrice, currency),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
{{
|
{{
|
||||||
$t("footer.savings.modalExplainationGrid", {
|
$t("footer.savings.modalServerStart", {
|
||||||
gridPrice: fmtPricePerKWh(gridPrice, currency),
|
since: fmtTimeAgo(secondsSinceStart()),
|
||||||
})
|
|
||||||
}}</span
|
|
||||||
>,
|
|
||||||
<span class="text-nowrap">
|
|
||||||
{{
|
|
||||||
$t("footer.savings.modalExplainationFeedIn", {
|
|
||||||
feedInPrice: fmtPricePerKWh(feedInPrice, currency),
|
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</p>
|
||||||
<br />
|
|
||||||
{{
|
|
||||||
$t("footer.savings.modalServerStart", {
|
|
||||||
since: fmtTimeAgo(secondsSinceStart()),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<hr class="mb-4" />
|
<hr class="mb-4" />
|
||||||
|
|
||||||
<Sponsor :sponsor="sponsor" class="mb-4" />
|
<Sponsor :sponsor="sponsor" />
|
||||||
|
</div>
|
||||||
<p class="small text-muted mb-0">
|
|
||||||
<strong class="text-primary">
|
|
||||||
<fa-icon icon="flask"></fa-icon>
|
|
||||||
{{ $t("footer.savings.experimentalLabel") }}:
|
|
||||||
</strong>
|
|
||||||
{{ $t("footer.savings.experimentalText") }}
|
|
||||||
<a
|
|
||||||
href="https://github.com/evcc-io/evcc/discussions/2104"
|
|
||||||
target="_blank"
|
|
||||||
>GitHub Discussions</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import formatter from "../mixins/formatter";
|
import formatter from "../mixins/formatter";
|
||||||
import Sponsor from "./Sponsor.vue";
|
import Sponsor from "./Sponsor.vue";
|
||||||
|
import "@h2d2/shopicons/es/filled/square";
|
||||||
|
import "@h2d2/shopicons/es/regular/sun";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Savings",
|
name: "Savings",
|
||||||
|
|
@ -180,6 +190,9 @@ export default {
|
||||||
percent() {
|
percent() {
|
||||||
return Math.round(this.selfConsumptionPercent) || 0;
|
return Math.round(this.selfConsumptionPercent) || 0;
|
||||||
},
|
},
|
||||||
|
noData() {
|
||||||
|
return this.totalCharged === 0;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
secondsSinceStart() {
|
secondsSinceStart() {
|
||||||
|
|
@ -196,7 +209,7 @@ export default {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.modal.fade .modal-dialog {
|
.modal.fade .modal-dialog {
|
||||||
transition: transform 0.4s ease;
|
transition: transform var(--evcc-transition-medium) ease;
|
||||||
transform: translate(0, 150px);
|
transform: translate(0, 150px);
|
||||||
}
|
}
|
||||||
.modal.show .modal-dialog {
|
.modal.show .modal-dialog {
|
||||||
|
|
@ -206,12 +219,13 @@ export default {
|
||||||
height: calc(100% - 0.5rem);
|
height: calc(100% - 0.5rem);
|
||||||
}
|
}
|
||||||
.modal-content {
|
.modal-content {
|
||||||
border-radius: 1rem 1rem 0 0;
|
border-end-end-radius: 0;
|
||||||
|
border-end-start-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
height: 1.6rem;
|
height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-item--self {
|
.chart-item--self {
|
||||||
|
|
@ -221,12 +235,12 @@ export default {
|
||||||
background-color: var(--evcc-grid);
|
background-color: var(--evcc-grid);
|
||||||
}
|
}
|
||||||
.chart-item--no-data {
|
.chart-item--no-data {
|
||||||
background-color: var(--bs-gray);
|
background-color: var(--bs-gray-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-item {
|
.chart-item {
|
||||||
transition-property: width;
|
transition-property: width;
|
||||||
transition-duration: 500ms;
|
transition-duration: var(--evcc-transition-medium);
|
||||||
transition-timing-function: linear;
|
transition-timing-function: linear;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import Site from "./Site.vue";
|
import Site from "./Site.vue";
|
||||||
import i18n from "../i18n";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Main/Site",
|
title: "Main/Site",
|
||||||
|
|
@ -7,11 +6,12 @@ export default {
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Template = (args, { argTypes }) => ({
|
const Template = (args) => ({
|
||||||
i18n,
|
setup() {
|
||||||
props: Object.keys(argTypes),
|
return { args };
|
||||||
|
},
|
||||||
components: { Site },
|
components: { Site },
|
||||||
template: '<Site v-bind="$props"></Site>',
|
template: '<Site v-bind="args"></Site>',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Base = Template.bind({});
|
export const Base = Template.bind({});
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-grow-1 d-flex flex-column">
|
<div class="d-flex flex-column site">
|
||||||
<h3 class="d-none d-md-block my-4">
|
<div class="container px-4 top-area">
|
||||||
{{ siteTitle || "Home" }}
|
<div class="d-flex justify-content-between align-items-center my-3">
|
||||||
</h3>
|
<h1 class="d-block my-0">
|
||||||
<Energyflow v-bind="energyflow" />
|
{{ siteTitle || "evcc" }}
|
||||||
<hr class="w-100 my-4" />
|
</h1>
|
||||||
<div class="flex-grow-1 d-flex justify-content-around flex-column">
|
<div class="d-flex">
|
||||||
<template v-for="(loadpoint, id) in loadpoints">
|
<Notifications :notifications="notifications" class="me-2" />
|
||||||
<hr v-if="id > 0" :key="id + '_hr'" class="w-100 my-4" />
|
<TopNavigation v-bind="topNavigation" />
|
||||||
<Loadpoint
|
</div>
|
||||||
v-bind="loadpoint"
|
</div>
|
||||||
:id="id"
|
<Energyflow v-bind="energyflow" />
|
||||||
:key="id"
|
</div>
|
||||||
:single="loadpoints.length === 1"
|
<div class="d-flex flex-column justify-content-between content-area pt-4">
|
||||||
/>
|
<Loadpoints class="mt-1 mt-sm-2 flex-grow-1" :loadpoints="loadpoints" />
|
||||||
</template>
|
<Vehicles v-if="$hiddenFeatures" />
|
||||||
|
<Footer v-bind="footer"></Footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Energyflow from "./Energyflow";
|
import "@h2d2/shopicons/es/regular/arrowup";
|
||||||
import Loadpoint from "./Loadpoint";
|
import TopNavigation from "./TopNavigation.vue";
|
||||||
|
import Notifications from "./Notifications.vue";
|
||||||
|
import Energyflow from "./Energyflow/Energyflow.vue";
|
||||||
|
import Loadpoints from "./Loadpoints.vue";
|
||||||
|
import Vehicles from "./Vehicles.vue";
|
||||||
|
import Footer from "./Footer.vue";
|
||||||
import formatter from "../mixins/formatter";
|
import formatter from "../mixins/formatter";
|
||||||
import collector from "../mixins/collector";
|
import collector from "../mixins/collector";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Site",
|
name: "Site",
|
||||||
components: { Loadpoint, Energyflow },
|
components: { Loadpoints, Energyflow, Footer, Notifications, TopNavigation, Vehicles },
|
||||||
mixins: [formatter, collector],
|
mixins: [formatter, collector],
|
||||||
props: {
|
props: {
|
||||||
loadpoints: Array,
|
loadpoints: Array,
|
||||||
|
|
||||||
|
notifications: Array,
|
||||||
|
|
||||||
// details
|
// details
|
||||||
gridConfigured: Boolean,
|
gridConfigured: Boolean,
|
||||||
gridPower: Number,
|
gridPower: Number,
|
||||||
|
|
@ -44,6 +52,30 @@ export default {
|
||||||
gridCurrents: Array,
|
gridCurrents: Array,
|
||||||
prioritySoC: Number,
|
prioritySoC: Number,
|
||||||
siteTitle: String,
|
siteTitle: String,
|
||||||
|
|
||||||
|
auth: Object,
|
||||||
|
|
||||||
|
// footer
|
||||||
|
currency: String,
|
||||||
|
savingsAmount: Number,
|
||||||
|
savingsEffectivePrice: Number,
|
||||||
|
savingsGridCharged: Number,
|
||||||
|
savingsSelfConsumptionCharged: Number,
|
||||||
|
savingsSelfConsumptionPercent: Number,
|
||||||
|
savingsSince: Number,
|
||||||
|
savingsTotalCharged: Number,
|
||||||
|
tariffFeedIn: Number,
|
||||||
|
tariffGrid: Number,
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
availableVersion: null,
|
||||||
|
releaseNotes: null,
|
||||||
|
hasUpdater: null,
|
||||||
|
uploadMessage: null,
|
||||||
|
uploadProgress: null,
|
||||||
|
sponsor: null,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
energyflow: function () {
|
energyflow: function () {
|
||||||
|
|
@ -58,6 +90,46 @@ export default {
|
||||||
return sum;
|
return sum;
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
|
topNavigation: function () {
|
||||||
|
const vehicleLogins = this.auth ? this.auth.vehicles : {};
|
||||||
|
return { vehicleLogins };
|
||||||
|
},
|
||||||
|
footer: function () {
|
||||||
|
return {
|
||||||
|
version: {
|
||||||
|
installed: window.evcc.version,
|
||||||
|
available: this.availableVersion,
|
||||||
|
releaseNotes: this.releaseNotes,
|
||||||
|
hasUpdater: this.hasUpdater,
|
||||||
|
uploadMessage: this.uploadMessage,
|
||||||
|
uploadProgress: this.uploadProgress,
|
||||||
|
},
|
||||||
|
sponsor: this.sponsor,
|
||||||
|
savings: {
|
||||||
|
since: this.savingsSince,
|
||||||
|
totalCharged: this.savingsTotalCharged,
|
||||||
|
gridCharged: this.savingsGridCharged,
|
||||||
|
selfConsumptionCharged: this.savingsSelfConsumptionCharged,
|
||||||
|
amount: this.savingsAmount,
|
||||||
|
effectivePrice: this.savingsEffectivePrice,
|
||||||
|
selfConsumptionPercent: this.savingsSelfConsumptionPercent,
|
||||||
|
gridPrice: this.tariffGrid,
|
||||||
|
feedInPrice: this.tariffFeedIn,
|
||||||
|
currency: this.currency,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.site {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.content-area {
|
||||||
|
background-color: var(--bs-gray-dark);
|
||||||
|
color: var(--bs-white);
|
||||||
|
flex-grow: 1;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import Sponsor from "./Sponsor.vue";
|
|
||||||
import i18n from "../i18n";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "Main/Footer/Sponsor",
|
|
||||||
component: Sponsor,
|
|
||||||
argTypes: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const Template = (args, { argTypes }) => ({
|
|
||||||
i18n,
|
|
||||||
props: Object.keys(argTypes),
|
|
||||||
components: { Sponsor },
|
|
||||||
template: '<Sponsor v-bind="$props"></Sponsor>',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Standard = Template.bind({});
|
|
||||||
Standard.args = {
|
|
||||||
sponsor: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IsSponsor = Template.bind({});
|
|
||||||
IsSponsor.args = {
|
|
||||||
sponsor: "naltatis",
|
|
||||||
};
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="sponsor">
|
<div v-if="sponsor || true">
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
{{ $t("footer.sponsor.thanks", { sponsor }) }}
|
{{ $t("footer.sponsor.thanks", { sponsor }) }}
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex justify-content-center flex-column">
|
<div class="d-flex justify-content-center align-items-center flex-column">
|
||||||
<button
|
<button
|
||||||
ref="confetti"
|
ref="confetti"
|
||||||
class="btn btn-primary mb-2 confetti-button bg-evcc"
|
class="btn btn btn-outline-primary mb-2 confetti-button bg-evcc w-75 rounded"
|
||||||
@click="surprise"
|
@click="surprise"
|
||||||
>
|
>
|
||||||
<fa-icon :icon="['fas', 'heart']" class="icon me-1 solid"></fa-icon>
|
<shopicon-regular-heart class="me-1 d-inline-block"></shopicon-regular-heart>
|
||||||
{{ $t("footer.sponsor.confetti") }}
|
{{ $t("footer.sponsor.confetti") }}
|
||||||
</button>
|
</button>
|
||||||
<a href="https://evcc.io/sticker" target="_blank" class="small text-muted text-center">
|
<a href="https://evcc.io/sticker" target="_blank" class="small text-muted">
|
||||||
{{ $t("footer.sponsor.sticker") }}
|
{{ $t("footer.sponsor.sticker") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -21,13 +21,13 @@
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
{{ $t("footer.sponsor.supportUs") }}
|
{{ $t("footer.sponsor.supportUs") }}
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex justify-content-center flex-column">
|
<div class="d-flex justify-content-center align-items-center flex-column">
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/sponsors/andig"
|
href="https://github.com/sponsors/andig"
|
||||||
class="btn btn-outline-primary mb-2"
|
class="btn btn-outline-primary mb-2 w-75"
|
||||||
>
|
>
|
||||||
<fa-icon :icon="['far', 'heart']" class="icon me-1"></fa-icon>
|
<shopicon-regular-heart class="me-1 d-inline-block"></shopicon-regular-heart>
|
||||||
{{ $t("footer.sponsor.becomeSponsor") }}
|
{{ $t("footer.sponsor.becomeSponsor") }}
|
||||||
</a>
|
</a>
|
||||||
<div class="small text-muted text-center">
|
<div class="small text-muted text-center">
|
||||||
|
|
@ -39,6 +39,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import confetti from "canvas-confetti";
|
import confetti from "canvas-confetti";
|
||||||
|
import "@h2d2/shopicons/es/regular/heart";
|
||||||
|
import "@h2d2/shopicons/es/filled/heart";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Sponsor",
|
name: "Sponsor",
|
||||||
|
|
|
||||||
|
|
@ -1,111 +1,123 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<LabelAndValue class="flex-grow-1" :label="$t('main.targetCharge.title')">
|
||||||
class="target-time-button btn btn-link btn-sm pe-0"
|
<button
|
||||||
:class="{
|
class="btn btn-link p-0 value text-end"
|
||||||
invisible: !targetSoC,
|
:class="buttonIsEnabled ? 'text-white' : 'text-gray'"
|
||||||
'text-dark': targetTimeActive,
|
data-bs-toggle="modal"
|
||||||
'text-secondary': !targetTimeActive,
|
:data-bs-target="`#${modalId}`"
|
||||||
}"
|
>
|
||||||
data-bs-toggle="modal"
|
<strong v-if="targetChargeEnabled">{{ targetTimeLabel() }}</strong>
|
||||||
:data-bs-target="`#${modalId}`"
|
<span v-else>{{ $t("main.targetCharge.setTargetTime") }}</span>
|
||||||
>
|
</button>
|
||||||
{{ targetTimeLabel() }}<fa-icon class="ms-1" icon="clock"></fa-icon>
|
</LabelAndValue>
|
||||||
</button>
|
|
||||||
<div :id="modalId" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
|
<Teleport to="body">
|
||||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
<div
|
||||||
<div class="modal-content">
|
:id="modalId"
|
||||||
<div class="modal-header">
|
class="modal fade text-dark"
|
||||||
<h5 class="modal-title">
|
data-bs-backdrop="true"
|
||||||
{{ $t("main.targetCharge.modalTitle") }}
|
tabindex="-1"
|
||||||
</h5>
|
role="dialog"
|
||||||
<button
|
aria-hidden="true"
|
||||||
type="button"
|
>
|
||||||
class="btn-close"
|
<div
|
||||||
data-bs-dismiss="modal"
|
class="modal-dialog modal-dialog-centered modal-dialog-scrollable"
|
||||||
aria-label="Close"
|
role="document"
|
||||||
></button>
|
>
|
||||||
</div>
|
<div class="modal-content">
|
||||||
<form @submit.prevent="setTargetTime">
|
<div class="modal-header">
|
||||||
<div class="modal-body">
|
<h5 class="modal-title">
|
||||||
<div class="form-group">
|
{{ $t("main.targetCharge.modalTitle") }}
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
</h5>
|
||||||
<label
|
|
||||||
for="targetTimeLabel"
|
|
||||||
class="mb-3"
|
|
||||||
v-html="$t('main.targetCharge.description', { targetSoC })"
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
|
||||||
<div
|
|
||||||
class="d-flex justify-content-between"
|
|
||||||
:style="{ 'max-width': '350px' }"
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
v-model="selectedDay"
|
|
||||||
class="form-select me-2"
|
|
||||||
:style="{ 'flex-basis': '60%' }"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="opt in dayOptions()"
|
|
||||||
:key="opt.value"
|
|
||||||
:value="opt.value"
|
|
||||||
>
|
|
||||||
{{ opt.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<input
|
|
||||||
v-model="selectedTime"
|
|
||||||
type="time"
|
|
||||||
class="form-control ms-2"
|
|
||||||
:style="{ 'flex-basis': '40%' }"
|
|
||||||
:step="60 * 5"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-if="!selectedTargetTimeValid" class="text-danger mb-0">
|
|
||||||
{{ $t("main.targetCharge.targetIsInThePast") }}
|
|
||||||
</p>
|
|
||||||
<p class="small mt-3 text-muted">
|
|
||||||
<strong class="text-primary">
|
|
||||||
<fa-icon icon="flask"></fa-icon>
|
|
||||||
{{ $t("main.targetCharge.experimentalLabel") }}:
|
|
||||||
</strong>
|
|
||||||
{{ $t("main.targetCharge.experimentalText") }}
|
|
||||||
<a
|
|
||||||
href="https://github.com/evcc-io/evcc/discussions/1433"
|
|
||||||
target="_blank"
|
|
||||||
>GitHub Discussions</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer d-flex justify-content-between">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-secondary"
|
class="btn-close"
|
||||||
data-bs-dismiss="modal"
|
data-bs-dismiss="modal"
|
||||||
@click="removeTargetTime"
|
aria-label="Close"
|
||||||
>
|
></button>
|
||||||
{{ $t("main.targetCharge.remove") }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary"
|
|
||||||
data-bs-dismiss="modal"
|
|
||||||
:disabled="!selectedTargetTimeValid"
|
|
||||||
>
|
|
||||||
{{ $t("main.targetCharge.activate") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<form @submit.prevent="setTargetTime">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<label for="targetTimeLabel" class="mb-3">
|
||||||
|
{{ $t("main.targetCharge.description", { targetSoC }) }}
|
||||||
|
</label>
|
||||||
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
|
<div
|
||||||
|
class="d-flex justify-content-between"
|
||||||
|
:style="{ 'max-width': '350px' }"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-model="selectedDay"
|
||||||
|
class="form-select me-2"
|
||||||
|
:style="{ 'flex-basis': '60%' }"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="opt in dayOptions()"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
v-model="selectedTime"
|
||||||
|
type="time"
|
||||||
|
class="form-control ms-2"
|
||||||
|
:style="{ 'flex-basis': '40%' }"
|
||||||
|
:step="60 * 5"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="!selectedTargetTimeValid" class="text-danger mb-0">
|
||||||
|
{{ $t("main.targetCharge.targetIsInThePast") }}
|
||||||
|
</p>
|
||||||
|
<p class="small mt-3 text-muted mb-0">
|
||||||
|
<strong class="text-evcc">
|
||||||
|
{{ $t("main.targetCharge.experimentalLabel") }}:
|
||||||
|
</strong>
|
||||||
|
{{ $t("main.targetCharge.experimentalText") }}
|
||||||
|
<a
|
||||||
|
href="https://github.com/evcc-io/evcc/discussions/1433"
|
||||||
|
target="_blank"
|
||||||
|
>GitHub Discussions</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer d-flex justify-content-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
@click="removeTargetTime"
|
||||||
|
>
|
||||||
|
{{ $t("main.targetCharge.remove") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
:disabled="!selectedTargetTimeValid"
|
||||||
|
>
|
||||||
|
{{ $t("main.targetCharge.activate") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import "@h2d2/shopicons/es/filled/plus";
|
||||||
|
import "@h2d2/shopicons/es/filled/edit";
|
||||||
|
import LabelAndValue from "./LabelAndValue.vue";
|
||||||
|
|
||||||
import formatter from "../mixins/formatter";
|
import formatter from "../mixins/formatter";
|
||||||
|
|
||||||
const DEFAULT_TARGET_TIME = "7:00";
|
const DEFAULT_TARGET_TIME = "7:00";
|
||||||
|
|
@ -113,13 +125,16 @@ const LAST_TARGET_TIME_KEY = "last_target_time";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "TargetCharge",
|
name: "TargetCharge",
|
||||||
|
components: { LabelAndValue },
|
||||||
mixins: [formatter],
|
mixins: [formatter],
|
||||||
props: {
|
props: {
|
||||||
id: Number,
|
id: [String, Number],
|
||||||
targetTime: String,
|
targetTime: String,
|
||||||
targetTimeActive: Boolean,
|
targetTimeActive: Boolean,
|
||||||
targetSoC: Number,
|
targetSoC: Number,
|
||||||
|
disabled: Boolean,
|
||||||
},
|
},
|
||||||
|
emits: ["target-time-updated", "target-time-removed"],
|
||||||
data: function () {
|
data: function () {
|
||||||
return { selectedDay: null, selectedTime: null };
|
return { selectedDay: null, selectedTime: null };
|
||||||
},
|
},
|
||||||
|
|
@ -127,6 +142,9 @@ export default {
|
||||||
targetChargeEnabled: function () {
|
targetChargeEnabled: function () {
|
||||||
return this.targetTime;
|
return this.targetTime;
|
||||||
},
|
},
|
||||||
|
buttonIsEnabled: function () {
|
||||||
|
return this.targetChargeEnabled && !this.disabled;
|
||||||
|
},
|
||||||
selectedTargetTimeValid: function () {
|
selectedTargetTimeValid: function () {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return now < this.selectedTargetTime;
|
return now < this.selectedTargetTime;
|
||||||
|
|
@ -139,6 +157,12 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
targetTimeLabel: function () {
|
||||||
|
const targetDate = new Date(this.targetTime);
|
||||||
|
return this.$t("main.targetCharge.activeLabel", {
|
||||||
|
time: this.fmtAbsoluteDate(targetDate),
|
||||||
|
});
|
||||||
|
},
|
||||||
targetTime() {
|
targetTime() {
|
||||||
this.initInputFields();
|
this.initInputFields();
|
||||||
},
|
},
|
||||||
|
|
@ -223,3 +247,15 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.value {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.2;
|
||||||
|
border: none;
|
||||||
|
transition: color var(--evcc-transition-medium) linear;
|
||||||
|
}
|
||||||
|
.value:hover {
|
||||||
|
color: var(--bs-color-white);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
53
assets/js/components/TopNavigation.stories.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import "bootstrap";
|
||||||
|
import TopNavigation from "./TopNavigation.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Main/TopNavigation",
|
||||||
|
component: TopNavigation,
|
||||||
|
parameters: {
|
||||||
|
backgrounds: {
|
||||||
|
default: "light",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args) => ({
|
||||||
|
setup() {
|
||||||
|
return { args };
|
||||||
|
},
|
||||||
|
components: { TopNavigation },
|
||||||
|
template: `<div class="d-flex justify-content-end" style="padding-bottom: 18rem">
|
||||||
|
<TopNavigation v-bind="args"></TopNavigation>
|
||||||
|
</div>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
Default.args = {};
|
||||||
|
|
||||||
|
export const VehicleLogins = Template.bind({});
|
||||||
|
VehicleLogins.args = {
|
||||||
|
vehicleLogins: {
|
||||||
|
["Mercedes EQS"]: {
|
||||||
|
authenticated: true,
|
||||||
|
uri: "https://login-provider-a.test/",
|
||||||
|
},
|
||||||
|
["Nissan Leaf Pro"]: {
|
||||||
|
authenticated: false,
|
||||||
|
uri: "https://login-provider-b.test/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PendingVehicleLogins = Template.bind({});
|
||||||
|
PendingVehicleLogins.args = {
|
||||||
|
vehicleLogins: {
|
||||||
|
["Mercedes EQS"]: {
|
||||||
|
authenticated: true,
|
||||||
|
uri: "https://login-provider-a.test/",
|
||||||
|
},
|
||||||
|
["Nissan Leaf Pro"]: {
|
||||||
|
authenticated: true,
|
||||||
|
uri: "https://login-provider-b.test/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
104
assets/js/components/TopNavigation.vue
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
data-bs-target="#navbarNavAltMarkup"
|
||||||
|
aria-controls="navbarNavAltMarkup"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
class="btn btn-sm btn-outline-secondary position-relative"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="logoutCount > 0"
|
||||||
|
class="position-absolute top-0 start-100 translate-middle p-2 bg-danger border border-light rounded-circle"
|
||||||
|
>
|
||||||
|
<span class="visually-hidden">login available</span>
|
||||||
|
</span>
|
||||||
|
<shopicon-regular-menu></shopicon-regular-menu>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="https://docs.evcc.io/blog/" target="_blank">
|
||||||
|
{{ $t("header.blog") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="https://docs.evcc.io/docs/Home/" target="_blank">
|
||||||
|
{{ $t("header.docs") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="https://github.com/evcc-io/evcc" target="_blank">
|
||||||
|
{{ $t("header.github") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="https://evcc.io/" target="_blank">
|
||||||
|
{{ $t("header.about") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<template v-if="providerLogins.length > 0">
|
||||||
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
<li>
|
||||||
|
<h6 class="dropdown-header">{{ $t("header.login") }}</h6>
|
||||||
|
</li>
|
||||||
|
<li v-for="login in providerLogins" :key="login.title">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dropdown-item"
|
||||||
|
@click="handleProviderAuthorization(login)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!login.loggedIn"
|
||||||
|
class="d-inline-block p-1 rounded-circle bg-danger border border-light rounded-circle"
|
||||||
|
></span>
|
||||||
|
{{ login.title }}
|
||||||
|
{{ $t(login.loggedIn ? "main.provider.logout" : "main.provider.login") }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import "@h2d2/shopicons/es/regular/menu";
|
||||||
|
import baseAPI from "../baseapi";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "TopNavigation",
|
||||||
|
props: {
|
||||||
|
vehicleLogins: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
logoutCount() {
|
||||||
|
return this.providerLogins.filter((login) => !login.loggedIn).length;
|
||||||
|
},
|
||||||
|
providerLogins() {
|
||||||
|
return Object.entries(this.vehicleLogins).map(([k, v]) => ({
|
||||||
|
title: k,
|
||||||
|
loggedIn: v.authenticated,
|
||||||
|
loginPath: v.uri + "/login",
|
||||||
|
logoutPath: v.uri + "/logout",
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleProviderAuthorization: async function (provider) {
|
||||||
|
if (!provider.loggedIn) {
|
||||||
|
baseAPI.post(provider.loginPath).then(function (response) {
|
||||||
|
window.location.href = response.data.loginUri;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
baseAPI.post(provider.logoutPath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import Vehicle from "./Vehicle.vue";
|
import Vehicle from "./Vehicle.vue";
|
||||||
import i18n from "../i18n";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Main/Vehicle",
|
title: "Main/Vehicle",
|
||||||
component: Vehicle,
|
component: Vehicle,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Template = (args, { argTypes }) => ({
|
const Template = (args) => ({
|
||||||
i18n,
|
setup() {
|
||||||
props: Object.keys(argTypes),
|
return { args };
|
||||||
|
},
|
||||||
components: { Vehicle },
|
components: { Vehicle },
|
||||||
template: '<Vehicle v-bind="$props"></Vehicle>',
|
template: '<Vehicle v-bind="args"></Vehicle>',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Base = Template.bind({});
|
export const Base = Template.bind({});
|
||||||
|
|
@ -20,6 +20,7 @@ Base.args = {
|
||||||
connected: true,
|
connected: true,
|
||||||
vehiclePresent: true,
|
vehiclePresent: true,
|
||||||
vehicleSoC: 42,
|
vehicleSoC: 42,
|
||||||
|
vehicleRange: 231,
|
||||||
targetSoC: 90,
|
targetSoC: 90,
|
||||||
id: 0,
|
id: 0,
|
||||||
};
|
};
|
||||||
|
|
@ -58,7 +59,6 @@ Charging.args = {
|
||||||
vehicleSoC: 66,
|
vehicleSoC: 66,
|
||||||
targetSoC: 90,
|
targetSoC: 90,
|
||||||
id: 0,
|
id: 0,
|
||||||
targetTimeHourSuggestion: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const hoursFromNow = function (hours) {
|
const hoursFromNow = function (hours) {
|
||||||
|
|
@ -110,6 +110,38 @@ MinCharge.args = {
|
||||||
id: 0,
|
id: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TimerPvEnable = Template.bind({});
|
||||||
|
TimerPvEnable.args = {
|
||||||
|
vehicleTitle: "Mein Auto",
|
||||||
|
enabled: false,
|
||||||
|
connected: true,
|
||||||
|
vehiclePresent: true,
|
||||||
|
vehicleSoC: 17,
|
||||||
|
charging: false,
|
||||||
|
targetSoC: 90,
|
||||||
|
phaseAction: "inactive",
|
||||||
|
phaseRemainingInterpolated: 0,
|
||||||
|
pvAction: "enable",
|
||||||
|
pvRemainingInterpolated: 32,
|
||||||
|
id: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TimerPvDisable = Template.bind({});
|
||||||
|
TimerPvDisable.args = {
|
||||||
|
vehicleTitle: "Mein Auto",
|
||||||
|
enabled: true,
|
||||||
|
connected: true,
|
||||||
|
vehiclePresent: true,
|
||||||
|
vehicleSoC: 17,
|
||||||
|
charging: true,
|
||||||
|
targetSoC: 90,
|
||||||
|
phaseAction: "inactive",
|
||||||
|
phaseRemainingInterpolated: 0,
|
||||||
|
pvAction: "disable",
|
||||||
|
pvRemainingInterpolated: 155,
|
||||||
|
id: 0,
|
||||||
|
};
|
||||||
|
|
||||||
export const UnknownVehicleConnected = Template.bind({});
|
export const UnknownVehicleConnected = Template.bind({});
|
||||||
UnknownVehicleConnected.args = {
|
UnknownVehicleConnected.args = {
|
||||||
vehicleTitle: "Mein Auto",
|
vehicleTitle: "Mein Auto",
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,118 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="vehicle p-4 pb-3">
|
||||||
<div class="mb-3">
|
<VehicleTitle v-bind="vehicleTitleProps" />
|
||||||
{{ vehicleTitle || $t("main.vehicle.fallbackName") }}
|
<VehicleStatus v-if="!parked" v-bind="vehicleStatus" class="mb-2" />
|
||||||
</div>
|
<VehicleSoc
|
||||||
<VehicleSoc v-bind="vehicleSocProps" @target-soc-updated="targetSocUpdated" />
|
v-bind="vehicleSocProps"
|
||||||
<VehicleSubline
|
class="mt-2 mb-4"
|
||||||
v-bind="vehicleSubline"
|
@target-soc-updated="targetSocUpdated"
|
||||||
class="my-1"
|
@target-soc-drag="targetSocDrag"
|
||||||
@target-time-updated="setTargetTime"
|
|
||||||
@target-time-removed="removeTargetTime"
|
|
||||||
/>
|
/>
|
||||||
|
<div v-if="vehiclePresent">
|
||||||
|
<div class="details d-flex flex-wrap justify-content-between">
|
||||||
|
<LabelAndValue
|
||||||
|
class="flex-grow-1 text-start"
|
||||||
|
:label="$t('main.vehicle.vehicleSoC')"
|
||||||
|
:value="`${vehicleSoC || '--'} %`"
|
||||||
|
:extraValue="vehicleRange ? `${vehicleRange} km` : null"
|
||||||
|
/>
|
||||||
|
<LabelAndValue
|
||||||
|
class="flex-grow-1 text-center"
|
||||||
|
:label="$t('main.vehicle.targetSoC')"
|
||||||
|
:value="`${displayTargetSoC} %`"
|
||||||
|
:extraValue="estimatedTargetRange"
|
||||||
|
/>
|
||||||
|
<TargetCharge
|
||||||
|
class="flex-grow-1 text-end target-charge"
|
||||||
|
v-bind="targetCharge"
|
||||||
|
:disabled="targetChargeDisabled"
|
||||||
|
@target-time-updated="setTargetTime"
|
||||||
|
@target-time-removed="removeTargetTime"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="$hiddenFeatures" class="d-flex justify-content-start">
|
||||||
|
<small>vor 5 Stunden</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import collector from "../mixins/collector";
|
import collector from "../mixins/collector";
|
||||||
|
import LabelAndValue from "./LabelAndValue.vue";
|
||||||
import VehicleSoc from "./VehicleSoc";
|
import VehicleTitle from "./VehicleTitle.vue";
|
||||||
import VehicleSubline from "./VehicleSubline";
|
import VehicleSoc from "./VehicleSoc.vue";
|
||||||
|
import VehicleStatus from "./VehicleStatus.vue";
|
||||||
|
import TargetCharge from "./TargetCharge.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Vehicle",
|
name: "Vehicle",
|
||||||
components: { VehicleSoc, VehicleSubline },
|
components: { VehicleTitle, VehicleSoc, VehicleStatus, LabelAndValue, TargetCharge },
|
||||||
mixins: [collector],
|
mixins: [collector],
|
||||||
props: {
|
props: {
|
||||||
id: Number,
|
id: [String, Number],
|
||||||
connected: Boolean,
|
connected: Boolean,
|
||||||
vehiclePresent: Boolean,
|
vehiclePresent: Boolean,
|
||||||
vehicleSoC: Number,
|
vehicleSoC: Number,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
charging: Boolean,
|
charging: Boolean,
|
||||||
minSoC: Number,
|
minSoC: Number,
|
||||||
|
vehicleRange: Number,
|
||||||
vehicleTitle: String,
|
vehicleTitle: String,
|
||||||
targetTimeActive: Boolean,
|
targetTimeActive: Boolean,
|
||||||
targetTimeHourSuggestion: Number,
|
|
||||||
targetTime: String,
|
targetTime: String,
|
||||||
|
targetTimeProjectedStart: String,
|
||||||
targetSoC: Number,
|
targetSoC: Number,
|
||||||
|
mode: String,
|
||||||
|
phaseAction: String,
|
||||||
|
phaseRemainingInterpolated: Number,
|
||||||
|
pvAction: String,
|
||||||
|
pvRemainingInterpolated: Number,
|
||||||
|
parked: Boolean,
|
||||||
|
},
|
||||||
|
emits: ["target-time-removed", "target-time-updated", "target-soc-updated"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
displayTargetSoC: this.targetSoC,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
vehicleSocProps: function () {
|
vehicleSocProps: function () {
|
||||||
return this.collectProps(VehicleSoc);
|
return this.collectProps(VehicleSoc);
|
||||||
},
|
},
|
||||||
vehicleSubline: function () {
|
vehicleStatus: function () {
|
||||||
return this.collectProps(VehicleSubline);
|
return this.collectProps(VehicleStatus);
|
||||||
|
},
|
||||||
|
vehicleTitleProps: function () {
|
||||||
|
return this.collectProps(VehicleTitle);
|
||||||
|
},
|
||||||
|
targetCharge: function () {
|
||||||
|
return this.collectProps(TargetCharge);
|
||||||
|
},
|
||||||
|
estimatedTargetRange: function () {
|
||||||
|
if (this.vehicleSoC > 10 && this.vehicleRange && this.displayTargetSoC) {
|
||||||
|
return (
|
||||||
|
Math.round(this.displayTargetSoC * (this.vehicleRange / this.vehicleSoC)) +
|
||||||
|
" km"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
targetChargeDisabled: function () {
|
||||||
|
return !this.connected || !["pv", "minpv"].includes(this.mode);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
targetSoC: function () {
|
||||||
|
this.displayTargetSoC = this.targetSoC;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
targetSocDrag: function (targetSoC) {
|
||||||
|
this.displayTargetSoC = targetSoC;
|
||||||
|
},
|
||||||
targetSocUpdated: function (targetSoC) {
|
targetSocUpdated: function (targetSoC) {
|
||||||
|
this.displayTargetSoC = targetSoC;
|
||||||
this.$emit("target-soc-updated", targetSoC);
|
this.$emit("target-soc-updated", targetSoC);
|
||||||
},
|
},
|
||||||
setTargetTime: function (targetTime) {
|
setTargetTime: function (targetTime) {
|
||||||
|
|
@ -58,3 +124,18 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.vehicle {
|
||||||
|
background-color: var(--bs-gray-dark);
|
||||||
|
border-radius: 1rem;
|
||||||
|
color: var(--bs-white);
|
||||||
|
}
|
||||||
|
.car-icon {
|
||||||
|
width: 1.75rem;
|
||||||
|
}
|
||||||
|
.details > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,39 +2,31 @@
|
||||||
<div class="vehicle-soc">
|
<div class="vehicle-soc">
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
<div
|
<div
|
||||||
|
v-if="connected || parked"
|
||||||
class="progress-bar"
|
class="progress-bar"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
:class="{
|
:class="{
|
||||||
|
[progressColor]: true,
|
||||||
'progress-bar-striped': charging,
|
'progress-bar-striped': charging,
|
||||||
'progress-bar-animated': charging,
|
'progress-bar-animated': charging,
|
||||||
[progressColor]: true,
|
|
||||||
}"
|
}"
|
||||||
:style="{ width: `${vehicleSoCDisplayWidth}%` }"
|
:style="{ width: `${vehicleSoCDisplayWidth}%` }"
|
||||||
>
|
></div>
|
||||||
{{ vehicleSoCDisplayValue }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="remainingSoCWidth > 0 && enabled"
|
v-if="remainingSoCWidth > 0 && enabled && connected"
|
||||||
class="progress-bar"
|
class="progress-bar bg-muted"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
:class="{
|
:class="progressColor"
|
||||||
[progressColor]: true,
|
|
||||||
'bg-muted': true,
|
|
||||||
}"
|
|
||||||
:style="{ width: `${remainingSoCWidth}%`, transition: 'none' }"
|
:style="{ width: `${remainingSoCWidth}%`, transition: 'none' }"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="target">
|
||||||
class="target"
|
|
||||||
:class="{ 'target--slider-hidden': allowSliderHiding && visibleTargetSoC === 100 }"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="target-label d-flex align-items-center justify-content-center"
|
class="target-label d-flex align-items-center justify-content-center"
|
||||||
:style="{ left: `${visibleTargetSoC}%` }"
|
:style="{ left: `${visibleTargetSoC}%` }"
|
||||||
>
|
></div>
|
||||||
{{ visibleTargetSoC }}%
|
|
||||||
</div>
|
|
||||||
<input
|
<input
|
||||||
|
v-if="vehiclePresent && (connected || parked)"
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
|
|
@ -62,11 +54,12 @@ export default {
|
||||||
charging: Boolean,
|
charging: Boolean,
|
||||||
minSoC: Number,
|
minSoC: Number,
|
||||||
targetSoC: Number,
|
targetSoC: Number,
|
||||||
|
parked: Boolean,
|
||||||
},
|
},
|
||||||
|
emits: ["target-soc-drag", "target-soc-updated"],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
selectedTargetSoC: null,
|
selectedTargetSoC: null,
|
||||||
allowSliderHiding: false,
|
|
||||||
interactionStartScreenY: null,
|
interactionStartScreenY: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -77,31 +70,7 @@ export default {
|
||||||
}
|
}
|
||||||
return 100;
|
return 100;
|
||||||
},
|
},
|
||||||
vehicleSoCDisplayValue: function () {
|
|
||||||
// no soc or no soc value
|
|
||||||
if (!this.vehiclePresent || !this.vehicleSoC || this.vehicleSoC < 0) {
|
|
||||||
let chargeStatus = this.$t("main.vehicleSoC.disconnected");
|
|
||||||
if (this.charging) {
|
|
||||||
chargeStatus = this.$t("main.vehicleSoC.charging");
|
|
||||||
} else if (this.enabled) {
|
|
||||||
chargeStatus = this.$t("main.vehicleSoC.ready");
|
|
||||||
} else if (this.connected) {
|
|
||||||
chargeStatus = this.$t("main.vehicleSoC.connected");
|
|
||||||
}
|
|
||||||
return chargeStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
// percent value if enough space
|
|
||||||
let vehicleSoC = this.vehicleSoC;
|
|
||||||
if (vehicleSoC >= 10) {
|
|
||||||
vehicleSoC += "%";
|
|
||||||
}
|
|
||||||
return vehicleSoC;
|
|
||||||
},
|
|
||||||
progressColor: function () {
|
progressColor: function () {
|
||||||
if (!this.connected) {
|
|
||||||
return "bg-light border";
|
|
||||||
}
|
|
||||||
if (this.minSoCActive) {
|
if (this.minSoCActive) {
|
||||||
return "bg-danger";
|
return "bg-danger";
|
||||||
}
|
}
|
||||||
|
|
@ -131,11 +100,6 @@ export default {
|
||||||
this.selectedTargetSoC = this.targetSoC;
|
this.selectedTargetSoC = this.targetSoC;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted: function () {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.allowSliderHiding = true;
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
changeTargetSoCStart: function (e) {
|
changeTargetSoCStart: function (e) {
|
||||||
const screenY = e.screenY || e.changedTouches[0].screenY;
|
const screenY = e.screenY || e.changedTouches[0].screenY;
|
||||||
|
|
@ -165,6 +129,8 @@ export default {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.selectedTargetSoC = e.target.value;
|
this.selectedTargetSoC = e.target.value;
|
||||||
|
|
||||||
|
this.$emit("target-soc-drag", this.selectedTargetSoC);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -172,10 +138,9 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.vehicle-soc {
|
.vehicle-soc {
|
||||||
--height: 38px;
|
--height: 32px;
|
||||||
--thumb-overlap: 3px;
|
--thumb-overlap: 6px;
|
||||||
--thumb-width: 3px;
|
--thumb-width: 12px;
|
||||||
--thumb-horizontal-padding: 15px;
|
|
||||||
--label-height: 26px;
|
--label-height: 26px;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: var(--height);
|
height: var(--height);
|
||||||
|
|
@ -199,16 +164,13 @@ export default {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--bs-gray-dark);
|
color: var(--bs-gray-dark);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.2s ease 1s;
|
|
||||||
}
|
}
|
||||||
.target-slider {
|
.target-slider {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(var(--thumb-overlap) * -1);
|
top: calc(var(--thumb-overlap) * -1);
|
||||||
left: calc(var(--thumb-horizontal-padding) * -1);
|
|
||||||
height: calc(100% + 2 * var(--thumb-overlap));
|
height: calc(100% + 2 * var(--thumb-overlap));
|
||||||
width: calc(100% + 2 * var(--thumb-horizontal-padding));
|
width: 100%;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
.target-slider:focus {
|
.target-slider:focus {
|
||||||
|
|
@ -231,50 +193,25 @@ export default {
|
||||||
.target-slider::-webkit-slider-thumb {
|
.target-slider::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: calc(var(--label-height) * -1);
|
margin-left: var(--thumb-width) / 2;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: var(--thumb-width);
|
width: var(--thumb-width);
|
||||||
padding: var(--label-height) var(--thumb-horizontal-padding) 0;
|
background-color: var(--evcc-dark-green);
|
||||||
box-sizing: content-box;
|
|
||||||
background-clip: content-box;
|
|
||||||
background-color: var(--bs-gray-dark);
|
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
border: none;
|
border: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.2s ease 1s;
|
border-radius: var(--thumb-overlap);
|
||||||
box-shadow: none;
|
box-shadow: 0 0 6px var(--bs-gray-dark);
|
||||||
}
|
}
|
||||||
.target-slider::-moz-range-thumb {
|
.target-slider::-moz-range-thumb {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: calc(var(--label-height) * -1);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: var(--thumb-width);
|
width: var(--thumb-width);
|
||||||
padding: 0 var(--thumb-horizontal-padding) 0;
|
background-color: var(--evcc-dark-green);
|
||||||
box-sizing: content-box;
|
|
||||||
background-clip: content-box;
|
|
||||||
background-color: var(--bs-gray-dark);
|
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
border: none;
|
border: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.5s ease 1s;
|
border-radius: var(--thumb-overlap);
|
||||||
}
|
box-shadow: 0 0 6px var(--bs-gray-dark);
|
||||||
/* auto-hide targetSoC marker at 100% */
|
|
||||||
.target--slider-hidden .target-slider::-webkit-slider-thumb,
|
|
||||||
.target--slider-hidden .target-label {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.target--slider-hidden .target-slider::-moz-range-thumb,
|
|
||||||
.target--slider-hidden .target-label {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.target:hover .target-slider::-webkit-slider-thumb,
|
|
||||||
.target:hover .target-label {
|
|
||||||
opacity: 1;
|
|
||||||
transition-delay: 0s;
|
|
||||||
}
|
|
||||||
.target:hover .target-slider::-moz-range-thumb,
|
|
||||||
.target:hover .target-label {
|
|
||||||
opacity: 1;
|
|
||||||
transition-delay: 0s;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
97
assets/js/components/VehicleStatus.vue
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<div class="d-block text-gray">{{ message }} </div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import collector from "../mixins/collector";
|
||||||
|
import formatter from "../mixins/formatter";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "VehicleStatus",
|
||||||
|
mixins: [collector, formatter],
|
||||||
|
props: {
|
||||||
|
vehicleSoC: Number,
|
||||||
|
minSoC: Number,
|
||||||
|
enabled: Boolean,
|
||||||
|
connected: Boolean,
|
||||||
|
charging: Boolean,
|
||||||
|
targetTime: String,
|
||||||
|
targetTimeProjectedStart: String,
|
||||||
|
phaseAction: String,
|
||||||
|
phaseRemainingInterpolated: Number,
|
||||||
|
pvAction: String,
|
||||||
|
pvRemainingInterpolated: Number,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
phaseTimerActive() {
|
||||||
|
return (
|
||||||
|
this.charging &&
|
||||||
|
this.phaseRemainingInterpolated > 0 &&
|
||||||
|
["scale1p", "scale3p"].includes(this.phaseAction)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pvTimerActive() {
|
||||||
|
return (
|
||||||
|
this.pvRemainingInterpolated > 0 && ["enable", "disable"].includes(this.pvAction)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
message: function () {
|
||||||
|
const t = (key, data) => {
|
||||||
|
return this.$t(`main.vehicleStatus.${key}`, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.connected) {
|
||||||
|
return t("disconnected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// min charge active
|
||||||
|
if (this.minSoC > 0 && this.vehicleSoC < this.minSoC) {
|
||||||
|
return t("minCharge", { soc: this.minSoC });
|
||||||
|
}
|
||||||
|
|
||||||
|
// target charage
|
||||||
|
if (this.targetTime) {
|
||||||
|
if (this.charging) {
|
||||||
|
return t("targetChargeActive");
|
||||||
|
}
|
||||||
|
if (this.enabled) {
|
||||||
|
return t("targetChargeWaitForVehicle");
|
||||||
|
}
|
||||||
|
if (this.targetTimeProjectedStart) {
|
||||||
|
return t("targetChargePlanned", {
|
||||||
|
time: this.fmtAbsoluteDate(new Date(this.targetTimeProjectedStart)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pvTimerActive && !this.enabled && this.pvAction === "enable") {
|
||||||
|
return t("pvEnable", {
|
||||||
|
remaining: this.fmtShortDuration(this.pvRemainingInterpolated, true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.enabled && !this.charging) {
|
||||||
|
return t("waitForVehicle");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pvTimerActive && this.charging && this.pvAction === "disable") {
|
||||||
|
return t("pvDisable", {
|
||||||
|
remaining: this.fmtShortDuration(this.pvRemainingInterpolated, true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.phaseTimerActive) {
|
||||||
|
return t(this.phaseAction, {
|
||||||
|
remaining: this.fmtShortDuration(this.phaseRemainingInterpolated, true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.charging) {
|
||||||
|
return t("charging");
|
||||||
|
}
|
||||||
|
|
||||||
|
return t("connected");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<small class="text-secondary">
|
|
||||||
<span v-if="minSoCActive">
|
|
||||||
<fa-icon class="text-muted me-1" icon="exclamation-circle"></fa-icon>
|
|
||||||
{{ $t("main.vehicleSubline.mincharge", { soc: minSoC }) }}
|
|
||||||
</span>
|
|
||||||
</small>
|
|
||||||
<TargetCharge
|
|
||||||
v-bind="targetCharge"
|
|
||||||
@target-time-updated="setTargetTime"
|
|
||||||
@target-time-removed="removeTargetTime"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import collector from "../mixins/collector";
|
|
||||||
import TargetCharge from "./TargetCharge.vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "VehicleSubline",
|
|
||||||
components: { TargetCharge },
|
|
||||||
mixins: [collector],
|
|
||||||
props: {
|
|
||||||
id: Number,
|
|
||||||
vehicleSoC: Number,
|
|
||||||
minSoC: Number,
|
|
||||||
targetTime: String,
|
|
||||||
targetTimeActive: Boolean,
|
|
||||||
targetTimeHourSuggestion: Number,
|
|
||||||
targetSoC: Number,
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
minSoCActive: function () {
|
|
||||||
return this.minSoC > 0 && this.vehicleSoC < this.minSoC;
|
|
||||||
},
|
|
||||||
targetCharge: function () {
|
|
||||||
return this.collectProps(TargetCharge);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setTargetTime: function (targetTime) {
|
|
||||||
this.$emit("target-time-updated", targetTime);
|
|
||||||
},
|
|
||||||
removeTargetTime: function () {
|
|
||||||
this.$emit("target-time-removed");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
44
assets/js/components/VehicleTitle.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div class="d-flex justify-content-between mb-3 align-items-center">
|
||||||
|
<h4 class="d-flex align-items-center m-0 flex-grow-1 overflow-hidden">
|
||||||
|
<shopicon-regular-car3
|
||||||
|
v-if="vehiclePresent || parked"
|
||||||
|
class="me-2 flex-shrink-0 car-icon"
|
||||||
|
></shopicon-regular-car3>
|
||||||
|
<shopicon-regular-cablecharge
|
||||||
|
v-else
|
||||||
|
class="me-2 flex-shrink-0 car-icon"
|
||||||
|
></shopicon-regular-cablecharge>
|
||||||
|
<span class="flex-grow-1 text-truncate"> {{ name }} </span>
|
||||||
|
</h4>
|
||||||
|
<button v-if="$hiddenFeatures" class="btn btn-link text-white p-0 flex-shrink-0">
|
||||||
|
<shopicon-filled-options size="s"></shopicon-filled-options>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import "@h2d2/shopicons/es/filled/options";
|
||||||
|
import "@h2d2/shopicons/es/regular/car3";
|
||||||
|
import "@h2d2/shopicons/es/regular/cablecharge";
|
||||||
|
|
||||||
|
import collector from "../mixins/collector";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "VehicleTitle",
|
||||||
|
mixins: [collector],
|
||||||
|
props: {
|
||||||
|
vehiclePresent: Boolean,
|
||||||
|
vehicleTitle: String,
|
||||||
|
parked: Boolean,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
name() {
|
||||||
|
if (this.vehiclePresent || this.parked) {
|
||||||
|
return this.vehicleTitle || this.$t("main.vehicle.fallbackName");
|
||||||
|
}
|
||||||
|
return this.$t("main.vehicle.noVehicle");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
104
assets/js/components/Vehicles.vue
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="container px-4 mb-3 mb-sm-4 d-flex justify-content-start align-items-center">
|
||||||
|
<h2 class="m-0">{{ $t("main.vehicles") }}</h2>
|
||||||
|
<button
|
||||||
|
class="btn btn-link d-flex text-white p-2 ms-1 refresh"
|
||||||
|
:class="{
|
||||||
|
'refresh--in-progress': refreshing,
|
||||||
|
}"
|
||||||
|
:disabled="refreshing"
|
||||||
|
@click="refresh"
|
||||||
|
>
|
||||||
|
<shopicon-regular-refresh class="refresh-icon"></shopicon-regular-refresh>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container vehicles px-0 mb-5" :class="`vehicles-${vehicles.length}`">
|
||||||
|
<Vehicle
|
||||||
|
v-for="(vehicle, index) in vehicles"
|
||||||
|
v-bind="vehicle"
|
||||||
|
:id="`vehicle_${index}`"
|
||||||
|
:key="index"
|
||||||
|
class="vehicle"
|
||||||
|
parked
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vehicle from "./Vehicle.vue";
|
||||||
|
import collector from "../mixins/collector";
|
||||||
|
import "@h2d2/shopicons/es/regular/refresh";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Vehicles",
|
||||||
|
components: { Vehicle },
|
||||||
|
mixins: [collector],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
refreshing: false,
|
||||||
|
// TODO: mock data for development
|
||||||
|
vehicles: [
|
||||||
|
{
|
||||||
|
vehiclePresent: true,
|
||||||
|
vehicleTitle: "Ford Mustang Mach-E",
|
||||||
|
vehicleSoC: 46,
|
||||||
|
vehicleRange: 182,
|
||||||
|
targetSoC: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vehiclePresent: true,
|
||||||
|
vehicleTitle: "Renault Twingo Electric",
|
||||||
|
vehicleSoC: 77,
|
||||||
|
vehicleRange: 98,
|
||||||
|
targetSoC: 90,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vehiclePresent: true,
|
||||||
|
vehicleTitle: "Blauer VW ID.4",
|
||||||
|
vehicleSoC: 16,
|
||||||
|
vehicleRange: 52,
|
||||||
|
minSoC: 35,
|
||||||
|
targetSoC: 60,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
refresh() {
|
||||||
|
this.refreshing = true;
|
||||||
|
// TODO: insert real implementation here
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.refreshing = false;
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.vehicles {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 2rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(310px, 1fr));
|
||||||
|
}
|
||||||
|
.vehicle {
|
||||||
|
border: 4px solid white;
|
||||||
|
}
|
||||||
|
.refresh--in-progress {
|
||||||
|
animation: rotation 1s infinite cubic-bezier(0.37, 0, 0.63, 1);
|
||||||
|
}
|
||||||
|
.refresh-icon {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
@keyframes rotation {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import Version from "./Version.vue";
|
import Version from "./Version.vue";
|
||||||
import i18n from "../i18n";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Main/Footer/Version",
|
title: "Main/Footer/Version",
|
||||||
|
|
@ -7,11 +6,12 @@ export default {
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Template = (args, { argTypes }) => ({
|
const Template = (args) => ({
|
||||||
i18n,
|
setup() {
|
||||||
props: Object.keys(argTypes),
|
return { args };
|
||||||
|
},
|
||||||
components: { Version },
|
components: { Version },
|
||||||
template: '<Version v-bind="$props"></Version>',
|
template: '<Version v-bind="args"></Version>',
|
||||||
});
|
});
|
||||||
|
|
||||||
const releaseNotes =
|
const releaseNotes =
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,25 @@
|
||||||
v-if="commit"
|
v-if="commit"
|
||||||
:href="githubHashUrl"
|
:href="githubHashUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="btn btn-link ps-0 text-decoration-none link-dark text-nowrap"
|
class="btn btn-link ps-0 text-decoration-none text-white text-nowrap d-flex align-items-end"
|
||||||
>
|
>
|
||||||
<fa-icon icon="moon" class="me-2 text-muted"></fa-icon>v{{ installed }}
|
<shopicon-regular-moonstars class="me-2 text-gray-medium"></shopicon-regular-moonstars>
|
||||||
<span class="text-muted">[{{ commit }}]</span>
|
v{{ installed }}
|
||||||
|
<span class="ms-2 text-gray-medium">[{{ commit }}]</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
v-else-if="newVersionAvailable"
|
v-else-if="newVersionAvailable"
|
||||||
href="#"
|
href="#"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#updateModal"
|
data-bs-target="#updateModal"
|
||||||
class="btn btn-link ps-0 text-decoration-none link-dark text-nowrap"
|
class="btn btn-link ps-0 text-decoration-none text-white text-nowrap d-flex align-items-end"
|
||||||
>
|
>
|
||||||
<fa-icon icon="gift" class="icon me-2"></fa-icon>v{{ installed }}
|
<shopicon-regular-gift class="me-2 text-evcc"></shopicon-regular-gift>
|
||||||
<span class="d-none d-xs-inline d-sm-none text-muted text-decoration-underline">
|
v{{ installed }}
|
||||||
|
<span class="ms-2 d-none d-xs-block d-sm-none text-decoration-underline">
|
||||||
{{ $t("footer.version.availableShort") }}
|
{{ $t("footer.version.availableShort") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="d-none d-sm-inline text-muted text-decoration-underline">
|
<span class="ms-2 d-none d-sm-block text-gray-medium text-decoration-underline">
|
||||||
{{ $t("footer.version.availableLong") }}
|
{{ $t("footer.version.availableLong") }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -28,17 +30,19 @@
|
||||||
v-else
|
v-else
|
||||||
:href="releaseNotesUrl(installed)"
|
:href="releaseNotesUrl(installed)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="btn btn-link ps-0 text-decoration-none link-dark text-nowrap"
|
class="btn btn-link text-white ps-0 text-decoration-none text-nowrap d-flex align-items-end"
|
||||||
>
|
>
|
||||||
<span class="d-inline d-xs-none d-sm-none">{{
|
<Logo class="logo me-2" />
|
||||||
$t("footer.version.versionShort", { installed })
|
v{{ installed }}
|
||||||
}}</span>
|
|
||||||
<span class="d-none d-xs-inline d-sm-inline">{{
|
|
||||||
$t("footer.version.versionLong", { installed })
|
|
||||||
}}</span>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div id="updateModal" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
|
<div
|
||||||
|
id="updateModal"
|
||||||
|
class="modal fade text-dark"
|
||||||
|
tabindex="-1"
|
||||||
|
role="dialog"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
|
@ -119,10 +123,14 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import "../icons";
|
import Logo from "./Logo.vue";
|
||||||
|
|
||||||
|
import "@h2d2/shopicons/es/regular/gift";
|
||||||
|
import "@h2d2/shopicons/es/regular/moonstars";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Version",
|
name: "Version",
|
||||||
|
components: { Logo },
|
||||||
props: {
|
props: {
|
||||||
installed: String,
|
installed: String,
|
||||||
available: String,
|
available: String,
|
||||||
|
|
@ -172,4 +180,8 @@ export default {
|
||||||
.icon {
|
.icon {
|
||||||
color: var(--evcc-dark-green);
|
color: var(--evcc-dark-green);
|
||||||
}
|
}
|
||||||
|
.logo {
|
||||||
|
height: 1.1rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.root >>> svg {
|
.root ::v-deep(svg) {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
5
assets/js/featureflags.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default {
|
||||||
|
install: (app) => {
|
||||||
|
app.config.globalProperties.$hiddenFeatures = window.localStorage["hidden_features"] === "true";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -3,12 +3,11 @@ export default {
|
||||||
docs: "Dokumentation",
|
docs: "Dokumentation",
|
||||||
blog: "Blog",
|
blog: "Blog",
|
||||||
github: "GitHub",
|
github: "GitHub",
|
||||||
login: "Anmeldungen",
|
login: "Fahrzeug Logins",
|
||||||
|
about: "Über evcc",
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
version: {
|
version: {
|
||||||
versionShort: "v{installed}",
|
|
||||||
versionLong: "Version {installed}",
|
|
||||||
availableShort: "Update",
|
availableShort: "Update",
|
||||||
availableLong: "Update verfügbar",
|
availableLong: "Update verfügbar",
|
||||||
modalTitle: "Update verfügbar",
|
modalTitle: "Update verfügbar",
|
||||||
|
|
@ -26,8 +25,8 @@ export default {
|
||||||
savings: {
|
savings: {
|
||||||
footerShort: "{percent}% Sonne",
|
footerShort: "{percent}% Sonne",
|
||||||
footerLong: "{percent}% Sonnenenergie",
|
footerLong: "{percent}% Sonnenenergie",
|
||||||
modalTitleShort: "{total} kWh geladen · {percent}% Sonne",
|
modalTitlePartOne: "{total} kWh geladen",
|
||||||
modalTitleLong: "{total} kWh geladen · {percent}% Sonnenenergie",
|
modalTitlePartTwo: "{percent}% Sonne",
|
||||||
modalChartGrid: "Netz {grid} kWh",
|
modalChartGrid: "Netz {grid} kWh",
|
||||||
modalChartSelf: "Sonne {self} kWh",
|
modalChartSelf: "Sonne {self} kWh",
|
||||||
modalSavingsPrice: "Effektiver Energiepreis",
|
modalSavingsPrice: "Effektiver Energiepreis",
|
||||||
|
|
@ -37,8 +36,6 @@ export default {
|
||||||
modalExplainationFeedIn: "Einspeisung {feedInPrice}",
|
modalExplainationFeedIn: "Einspeisung {feedInPrice}",
|
||||||
modalServerStart: "Seit Serverstart {since}.",
|
modalServerStart: "Seit Serverstart {since}.",
|
||||||
modalNoData: "noch nicht geladen",
|
modalNoData: "noch nicht geladen",
|
||||||
experimentalLabel: "experimentell",
|
|
||||||
experimentalText: "Unplausible Werte? Fragen zur Ansicht? Meld dich gerne in den",
|
|
||||||
},
|
},
|
||||||
sponsor: {
|
sponsor: {
|
||||||
thanks: "Danke für deine Unterstützung, {sponsor}! Das hilft uns bei der Weiterentwicklung.",
|
thanks: "Danke für deine Unterstützung, {sponsor}! Das hilft uns bei der Weiterentwicklung.",
|
||||||
|
|
@ -54,7 +51,6 @@ export default {
|
||||||
modalTitle: "Meldungen",
|
modalTitle: "Meldungen",
|
||||||
dismissAll: "Meldungen entfernen",
|
dismissAll: "Meldungen entfernen",
|
||||||
},
|
},
|
||||||
|
|
||||||
main: {
|
main: {
|
||||||
energyflow: {
|
energyflow: {
|
||||||
noEnergy: "Kein Energiefluss",
|
noEnergy: "Kein Energiefluss",
|
||||||
|
|
@ -69,21 +65,32 @@ export default {
|
||||||
pvExport: "Einspeisung",
|
pvExport: "Einspeisung",
|
||||||
},
|
},
|
||||||
mode: {
|
mode: {
|
||||||
title: "Modus",
|
offShort: "Aus",
|
||||||
stop: "Stop",
|
offLong: "Aus",
|
||||||
now: "Sofort",
|
nowShort: "Sofort",
|
||||||
|
nowLong: "Sofort",
|
||||||
minpvShort: "Min",
|
minpvShort: "Min",
|
||||||
minpvLong: "Min + PV",
|
minpvLong: "Min + PV",
|
||||||
pvShort: "PV",
|
pvShort: "PV",
|
||||||
pvLong: "Nur PV",
|
pvLong: "PV",
|
||||||
|
fast: "Schnell",
|
||||||
|
cheap: "Günstig",
|
||||||
},
|
},
|
||||||
loadpoint: {
|
loadpoint: {
|
||||||
fallbackName: "Ladepunkt",
|
fallbackName: "Ladepunkt",
|
||||||
remoteDisabledSoft: "{source}: Adaptives PV-Laden deaktiviert",
|
remoteDisabledSoft: "{source}: Adaptives PV-Laden deaktiviert",
|
||||||
remoteDisabledHard: "{source}: Deaktiviert",
|
remoteDisabledHard: "{source}: Deaktiviert",
|
||||||
|
power: "Leistung",
|
||||||
|
charged: "Geladen",
|
||||||
|
duration: "Dauer",
|
||||||
|
remaining: "Restzeit",
|
||||||
},
|
},
|
||||||
|
vehicles: "Parkplatz",
|
||||||
vehicle: {
|
vehicle: {
|
||||||
fallbackName: "Fahrzeug",
|
fallbackName: "Fahrzeug",
|
||||||
|
vehicleSoC: "Ladestand",
|
||||||
|
targetSoC: "Ladeziel",
|
||||||
|
noVehicle: "Kein Fahrzeug",
|
||||||
},
|
},
|
||||||
vehicleSoC: {
|
vehicleSoC: {
|
||||||
disconnected: "getrennt",
|
disconnected: "getrennt",
|
||||||
|
|
@ -91,51 +98,42 @@ export default {
|
||||||
ready: "bereit",
|
ready: "bereit",
|
||||||
connected: "verbunden",
|
connected: "verbunden",
|
||||||
},
|
},
|
||||||
vehicleSubline: {
|
vehicleStatus: {
|
||||||
mincharge: "Mindestladung bis {soc}%",
|
minCharge: "Mindestladung bis {soc}%.",
|
||||||
|
waitForVehicle: "Ladebereit. Warte auf Fahrzeug.",
|
||||||
|
charging: "Ladevorgang aktiv.",
|
||||||
|
targetChargePlanned: "Zielladen geplant. Ladung startet {time} Uhr.",
|
||||||
|
targetChargeWaitForVehicle: "Zielladen bereit. Warte auf Fahrzeug.",
|
||||||
|
targetChargeActive: "Zielladen aktiv.",
|
||||||
|
connected: "Verbunden.",
|
||||||
|
pvDisable: "Zu wenig Überschuss. Pausiere in {remaining}.",
|
||||||
|
pvEnable: "Überschuss verfügbar. Starte in {remaining}.",
|
||||||
|
scale1p: "Reduziere auf einphasig in {remaining}.",
|
||||||
|
scale3p: "Erhöhe auf dreiphasig in {remaining}.",
|
||||||
|
disconnected: "Nicht verbunden.",
|
||||||
|
unknown: "",
|
||||||
},
|
},
|
||||||
provider: {
|
provider: {
|
||||||
login: "anmelden",
|
login: "anmelden",
|
||||||
logout: "abmelden",
|
logout: "abmelden",
|
||||||
},
|
},
|
||||||
targetCharge: {
|
targetCharge: {
|
||||||
|
title: "Zielzeit",
|
||||||
inactiveLabel: "Zielzeit",
|
inactiveLabel: "Zielzeit",
|
||||||
activeLabel: "bis {time} Uhr",
|
activeLabel: "{time}",
|
||||||
modalTitle: "Zielzeit festlegen",
|
modalTitle: "Zielzeit festlegen",
|
||||||
description: "Wann soll das Fahrzeug auf <strong>{targetSoC}%</strong> geladen sein?",
|
setTargetTime: "keine",
|
||||||
|
description: "Wann soll das Fahrzeug auf {targetSoC}% geladen sein?",
|
||||||
today: "heute",
|
today: "heute",
|
||||||
tomorrow: "morgen",
|
tomorrow: "morgen",
|
||||||
targetIsInThePast: "Zeitpunkt liegt in der Vergangenheit.",
|
targetIsInThePast: "Zeitpunkt liegt in der Vergangenheit.",
|
||||||
remove: "Keine Zielzeit",
|
remove: "Entfernen",
|
||||||
activate: "Zielzeit aktivieren",
|
activate: "Aktivieren",
|
||||||
experimentalLabel: "experimentell",
|
experimentalLabel: "Experimentell",
|
||||||
experimentalText: `
|
experimentalText: `
|
||||||
Diese Funktion ist in einem frühen Stadium. Der Algorithmus ist noch
|
Dieses Feature funktioniert, ist aber noch nicht perfekt.
|
||||||
nicht perfekt. Die Zielzeit wird aktuell nicht persistiert - das
|
Bitte melde unerwartetes Verhalten in unseren
|
||||||
heißt sie geht beim Neustart verloren. Verlasse dich also noch nicht
|
|
||||||
zu sehr auf diese Funktion. Wir freuen uns aber über deine
|
|
||||||
Erfahrungen und Verbessungsvorschläge in den
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
loadpointDetails: {
|
|
||||||
power: "Leistung",
|
|
||||||
vehicleRange: "Reichweite",
|
|
||||||
charged: "Geladen",
|
|
||||||
duration: "Dauer",
|
|
||||||
remaining: "Restzeit",
|
|
||||||
tooltip: {
|
|
||||||
phases: {
|
|
||||||
scale1p: "Herunterschalten auf einphasig in {remaining}.",
|
|
||||||
scale3p: "Hochschalten auf dreiphasig in {remaining}.",
|
|
||||||
charge1p: "Lädt einphasig.",
|
|
||||||
charge2p: "Lädt zweiphasig.",
|
|
||||||
charge3p: "Lädt dreiphasig.",
|
|
||||||
},
|
|
||||||
pv: {
|
|
||||||
enable: "Überschuss verfügbar. Ladung startet in {remaining}.",
|
|
||||||
disable: "Zu wenig Überschuss. Ladung pausiert in {remaining}.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@ export default {
|
||||||
docs: "Documentation",
|
docs: "Documentation",
|
||||||
blog: "Blog",
|
blog: "Blog",
|
||||||
github: "GitHub",
|
github: "GitHub",
|
||||||
login: "Logins",
|
login: "Vehicle Logins",
|
||||||
|
about: "About evcc",
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
version: {
|
version: {
|
||||||
versionShort: "v{installed}",
|
|
||||||
versionLong: "Version {installed}",
|
|
||||||
availableShort: "update",
|
availableShort: "update",
|
||||||
availableLong: "update available",
|
availableLong: "update available",
|
||||||
modalTitle: "Update available",
|
modalTitle: "Update available",
|
||||||
|
|
@ -26,10 +25,10 @@ export default {
|
||||||
savings: {
|
savings: {
|
||||||
footerShort: "{percent}% solar",
|
footerShort: "{percent}% solar",
|
||||||
footerLong: "{percent}% solar energy",
|
footerLong: "{percent}% solar energy",
|
||||||
modalTitleShort: "{total} kWh charged · {percent}% solar",
|
modalTitlePartOne: "{total} kWh charged",
|
||||||
modalTitleLong: "{total} kWh charged · {percent}% solar energy",
|
modalTitlePartTwo: "{percent}% solar",
|
||||||
modalChartGrid: "Grid energy {grid} kWh",
|
modalChartGrid: "Grid {grid} kWh",
|
||||||
modalChartSelf: "Solar energy {self} kWh",
|
modalChartSelf: "Solar {self} kWh",
|
||||||
modalSavingsPrice: "Effective energy price",
|
modalSavingsPrice: "Effective energy price",
|
||||||
modalSavingsTotal: "Savings compared to grid",
|
modalSavingsTotal: "Savings compared to grid",
|
||||||
modalExplaination: "Calculation",
|
modalExplaination: "Calculation",
|
||||||
|
|
@ -37,8 +36,6 @@ export default {
|
||||||
modalExplainationFeedIn: "feed-in rate {feedInPrice}",
|
modalExplainationFeedIn: "feed-in rate {feedInPrice}",
|
||||||
modalServerStart: "since server start {since}.",
|
modalServerStart: "since server start {since}.",
|
||||||
modalNoData: "nothing charged yet",
|
modalNoData: "nothing charged yet",
|
||||||
experimentalLabel: "experimental",
|
|
||||||
experimentalText: "Implausible values? Questions about this view? Feel free to join our ",
|
|
||||||
},
|
},
|
||||||
sponsor: {
|
sponsor: {
|
||||||
thanks: "Thanks for your support, {sponsor}! It helps us with the further development.",
|
thanks: "Thanks for your support, {sponsor}! It helps us with the further development.",
|
||||||
|
|
@ -58,8 +55,8 @@ export default {
|
||||||
energyflow: {
|
energyflow: {
|
||||||
noEnergy: "No Energyflow",
|
noEnergy: "No Energyflow",
|
||||||
homePower: "Consumption",
|
homePower: "Consumption",
|
||||||
loadpoints: "Loadpoint | Loadpoint | {count} Loadpoints",
|
|
||||||
pvProduction: "Production",
|
pvProduction: "Production",
|
||||||
|
loadpoints: "Loadpoint | Loadpoint | {count} Loadpoints",
|
||||||
battery: "Battery",
|
battery: "Battery",
|
||||||
batteryCharge: "Battery charge",
|
batteryCharge: "Battery charge",
|
||||||
batteryDischarge: "Battery discharge",
|
batteryDischarge: "Battery discharge",
|
||||||
|
|
@ -68,21 +65,32 @@ export default {
|
||||||
pvExport: "Grid export",
|
pvExport: "Grid export",
|
||||||
},
|
},
|
||||||
mode: {
|
mode: {
|
||||||
title: "Mode",
|
offShort: "Stop",
|
||||||
stop: "Stop",
|
offLong: "Stop",
|
||||||
now: "Now",
|
nowShort: "Now",
|
||||||
|
nowLong: "Now",
|
||||||
minpvShort: "Min",
|
minpvShort: "Min",
|
||||||
minpvLong: "Min + PV",
|
minpvLong: "Min + PV",
|
||||||
pvShort: "PV",
|
pvShort: "PV",
|
||||||
pvLong: "PV only",
|
pvLong: "PV",
|
||||||
|
fast: "Fast",
|
||||||
|
cheap: "Cheap",
|
||||||
},
|
},
|
||||||
loadpoint: {
|
loadpoint: {
|
||||||
fallbackName: "Loadpoint",
|
fallbackName: "Loadpoint",
|
||||||
remoteDisabledSoft: "{source}: adaptive PV charging disabled",
|
remoteDisabledSoft: "{source}: adaptive PV charging disabled",
|
||||||
remoteDisabledHard: "{source}: disabled",
|
remoteDisabledHard: "{source}: disabled",
|
||||||
|
power: "Power",
|
||||||
|
charged: "Charged",
|
||||||
|
duration: "Duration",
|
||||||
|
remaining: "Remaining",
|
||||||
},
|
},
|
||||||
|
vehicles: "Parking",
|
||||||
vehicle: {
|
vehicle: {
|
||||||
fallbackName: "Vehicle",
|
fallbackName: "Vehicle",
|
||||||
|
vehicleSoC: "SoC",
|
||||||
|
targetSoC: "Limit",
|
||||||
|
noVehicle: "No Vehicle",
|
||||||
},
|
},
|
||||||
vehicleSoC: {
|
vehicleSoC: {
|
||||||
disconnected: "disconnected",
|
disconnected: "disconnected",
|
||||||
|
|
@ -90,51 +98,42 @@ export default {
|
||||||
ready: "ready",
|
ready: "ready",
|
||||||
connected: "connected",
|
connected: "connected",
|
||||||
},
|
},
|
||||||
vehicleSubline: {
|
vehicleStatus: {
|
||||||
mincharge: "minimum charging to {soc}%",
|
minCharge: "minimum charging to {soc}%.",
|
||||||
|
waitForVehicle: "Ready. Waiting for vehicle.",
|
||||||
|
charging: "Charging.",
|
||||||
|
targetChargePlanned: "Target charge planned. Starting {start}.",
|
||||||
|
targetChargeWaitForVehicle: "Target charge ready. Wait for vehicle.",
|
||||||
|
targetChargeActive: "Target charge active.",
|
||||||
|
connected: "Connected.",
|
||||||
|
pvDisable: "Not enough surplus. Pausing in {remaining}.",
|
||||||
|
pvEnable: "Surplus available. Starging in {remaining}.",
|
||||||
|
scale1p: "Reduce to single phase in {remaining}.",
|
||||||
|
scale3p: "Increase to three phase in {remaining}.",
|
||||||
|
disconnected: "Disconnected.",
|
||||||
|
unknown: "",
|
||||||
},
|
},
|
||||||
provider: {
|
provider: {
|
||||||
login: "login",
|
login: "login",
|
||||||
logout: "logout",
|
logout: "logout",
|
||||||
},
|
},
|
||||||
targetCharge: {
|
targetCharge: {
|
||||||
|
title: "Target Time",
|
||||||
inactiveLabel: "Target time",
|
inactiveLabel: "Target time",
|
||||||
activeLabel: "until {time}",
|
activeLabel: "{time}",
|
||||||
modalTitle: "Set Target Time",
|
modalTitle: "Set Target Time",
|
||||||
description: "When should the vehicle be charged to <strong>{targetSoC}%</strong>?",
|
setTargetTime: "none",
|
||||||
|
description: "When should the vehicle be charged to {targetSoC}%?",
|
||||||
today: "today",
|
today: "today",
|
||||||
tomorrow: "tomorrow",
|
tomorrow: "tomorrow",
|
||||||
targetIsInThePast: "The chosen time is in the past.",
|
targetIsInThePast: "The chosen time is in the past.",
|
||||||
remove: "Remove",
|
remove: "Remove",
|
||||||
activate: "Activate",
|
activate: "Activate",
|
||||||
experimentalLabel: "experimental",
|
experimentalLabel: "Experimental",
|
||||||
experimentalText: `
|
experimentalText: `
|
||||||
This function is at an early stage.
|
This feature works but isn't perfect yet.
|
||||||
The algorithm is not perfect yet.
|
Please report unexpected behaviour in our
|
||||||
The target time is currently not persisted - this means, it will be lost when your server restarts.
|
|
||||||
So do not rely too much on this function.
|
|
||||||
However, we look forward to your experiences and suggestions for improvement in the
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
loadpointDetails: {
|
|
||||||
power: "Power",
|
|
||||||
vehicleRange: "Range",
|
|
||||||
charged: "Charged",
|
|
||||||
duration: "Duration",
|
|
||||||
remaining: "Remaining",
|
|
||||||
tooltip: {
|
|
||||||
phases: {
|
|
||||||
scale1p: "Switching to single-phase in {remaining}.",
|
|
||||||
scale3p: "Switching to three-phase in {remaining}.",
|
|
||||||
charge1p: "Single-phase charging.",
|
|
||||||
charge2p: "Two-phase charging.",
|
|
||||||
charge3p: "Three-phase charging.",
|
|
||||||
},
|
|
||||||
pv: {
|
|
||||||
enable: "Solar available. Resume charging in {remaining}.",
|
|
||||||
disable: "Not enough solar. Pause charging in {remaining}.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import Vue from "vue";
|
import { createI18n } from "vue-i18n/index";
|
||||||
import VueI18n from "vue-i18n";
|
|
||||||
import de from "./de";
|
import de from "./de";
|
||||||
import en from "./en";
|
import en from "./en";
|
||||||
import it from "./it";
|
import it from "./it";
|
||||||
import lt from "./lt";
|
import lt from "./lt";
|
||||||
|
|
||||||
Vue.use(VueI18n);
|
|
||||||
|
|
||||||
const PREFERRED_LOCALE_KEY = "preferred_locale";
|
const PREFERRED_LOCALE_KEY = "preferred_locale";
|
||||||
|
|
||||||
function getBrowserLocale() {
|
function getBrowserLocale() {
|
||||||
|
|
@ -15,11 +12,10 @@ function getBrowserLocale() {
|
||||||
if (!navigatorLocale) {
|
if (!navigatorLocale) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const trimmedLocale = navigatorLocale.trim().split(/-|_/)[0];
|
return navigatorLocale.trim().split(/-|_/)[0];
|
||||||
return trimmedLocale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new VueI18n({
|
export default createI18n({
|
||||||
locale: window.localStorage[PREFERRED_LOCALE_KEY] || getBrowserLocale(),
|
locale: window.localStorage[PREFERRED_LOCALE_KEY] || getBrowserLocale(),
|
||||||
fallbackLocale: "en",
|
fallbackLocale: "en",
|
||||||
messages: { de, en, it, lt },
|
messages: { de, en, it, lt },
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
export default {
|
export default {
|
||||||
header: {
|
header: {
|
||||||
docs: "Dokumentacija",
|
docs: "Dokumentacija (Vokiečių k.)",
|
||||||
blog: "Tinklaraštis",
|
blog: "Tinklaraštis",
|
||||||
github: "GitHub",
|
github: "GitHub",
|
||||||
login: "Prisijungimas",
|
login: " Automobilių prisijungimai",
|
||||||
|
about: "Apie EVCC",
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
version: {
|
version: {
|
||||||
versionShort: "v{installed}",
|
|
||||||
versionLong: "Versija {installed}",
|
|
||||||
availableShort: "Naujinimas",
|
availableShort: "Naujinimas",
|
||||||
availableLong: "Prieinama naujesnė versija",
|
availableLong: "Yra naujesnė versija",
|
||||||
modalTitle: "Prieinama naujesnė versija",
|
modalTitle: "Yra naujesnė versija",
|
||||||
modalUpdateStarted: "Pasibaigus naujinimui EVCC startuos iš naujo..",
|
modalUpdateStarted: "Pasibaigus naujinimui EVCC startuos iš naujo..",
|
||||||
modalInstalledVersion: "Dabartinė versija",
|
modalInstalledVersion: "Dabartinė versija",
|
||||||
modalNoReleaseNotes: "Naujinimo detalių nėra. Daugiau informacijos rasite čia:",
|
modalNoReleaseNotes: "Naujinimo detalių nėra. Daugiau informacijos rasite čia:",
|
||||||
|
|
@ -25,8 +24,8 @@ export default {
|
||||||
savings: {
|
savings: {
|
||||||
footerShort: "{percent}% saulės",
|
footerShort: "{percent}% saulės",
|
||||||
footerLong: "{percent}% saulės energija",
|
footerLong: "{percent}% saulės energija",
|
||||||
modalTitleShort: "{total} kWh įkrauta · {percent}% saulės",
|
modalTitlePartOne: "{total} kWh įkrauta",
|
||||||
modalTitleLong: "{total} kWh įkrauta · {percent}% saulės energija",
|
modalTitlePartTwo: "{percent}% saulės",
|
||||||
modalChartGrid: "Energija iš tinklo {grid} kWh",
|
modalChartGrid: "Energija iš tinklo {grid} kWh",
|
||||||
modalChartSelf: "Saulės energija {self} kWh",
|
modalChartSelf: "Saulės energija {self} kWh",
|
||||||
modalSavingsPrice: "Faktinė energijos kaina",
|
modalSavingsPrice: "Faktinė energijos kaina",
|
||||||
|
|
@ -36,18 +35,15 @@ export default {
|
||||||
modalExplainationFeedIn: "Kompensavimas už patiektą į tinklą energiją {feedInPrice}",
|
modalExplainationFeedIn: "Kompensavimas už patiektą į tinklą energiją {feedInPrice}",
|
||||||
modalServerStart: "nuo EVCC starto {since}.",
|
modalServerStart: "nuo EVCC starto {since}.",
|
||||||
modalNoData: "įkrovimo duomenų dar nėra",
|
modalNoData: "įkrovimo duomenų dar nėra",
|
||||||
experimentalLabel: "eksperimentinis",
|
|
||||||
experimentalText:
|
|
||||||
"Neteisingi duomenys? Turite klausimų apie šiuos skaičiavimus? Prisijunkite prie mūsų",
|
|
||||||
},
|
},
|
||||||
sponsor: {
|
sponsor: {
|
||||||
thanks: "Ačiū, kad mus remiate {sponsor}! Tuo prisidedate prie projekto vystymo.",
|
thanks: "Ačiū, kad mus remiate {sponsor}! Taip prisidedate prie projekto vystymo.",
|
||||||
confetti: "Ar pasiruošę priimti rėmėjo konfeti?",
|
confetti: "Ar pasiruošę priimti rėmėjo konfeti?",
|
||||||
supportUs:
|
supportUs:
|
||||||
"Norime, kad efektyvesnis elektromobilių įkrovimas taptų pasiekiamas kuo daugiau žmonių. Tapdami rėmėjais prisidedate prie nuolatinio projekto vystymo ir palaikymo.",
|
"Norime, kad efektyvesnis elektromobilių įkrovimas saulės energija taptų pasiekiamas kuo daugiau žmonių. Remdami prisidedate prie nuolatinio projekto vystymo ir techninio palaikymo.",
|
||||||
sticker: "... ar evcc lipdukų?",
|
sticker: "... ar EVCC lipdukų?",
|
||||||
confettiPromise: "Gausite skaitmeninių lipdukų ir konfeti ;)",
|
confettiPromise: "Gausite lipdukų ir skaitmeninių konfeti ;)",
|
||||||
becomeSponsor: "Taptkite GitHub rėmėju!",
|
becomeSponsor: "Pradėkite GitHub rėmimą!",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
|
|
@ -58,8 +54,8 @@ export default {
|
||||||
energyflow: {
|
energyflow: {
|
||||||
noEnergy: "Energija neteka",
|
noEnergy: "Energija neteka",
|
||||||
homePower: "Namo suvartojimas",
|
homePower: "Namo suvartojimas",
|
||||||
loadpoints: "Įkroviklis | Įkroviklis | {count} Įkrovikliai",
|
|
||||||
pvProduction: "Gamyba",
|
pvProduction: "Gamyba",
|
||||||
|
loadpoints: "Įkroviklis | Įkroviklis | {count} Įkrovikliai",
|
||||||
battery: "Baterija",
|
battery: "Baterija",
|
||||||
batteryCharge: "Baterijos įkrovimas",
|
batteryCharge: "Baterijos įkrovimas",
|
||||||
batteryDischarge: "Baterijos iškrovimas",
|
batteryDischarge: "Baterijos iškrovimas",
|
||||||
|
|
@ -68,73 +64,75 @@ export default {
|
||||||
pvExport: "Tinklo eksportas",
|
pvExport: "Tinklo eksportas",
|
||||||
},
|
},
|
||||||
mode: {
|
mode: {
|
||||||
title: "Darbo režimas",
|
offShort: "Stop",
|
||||||
stop: "Stop",
|
offLong: "Stop",
|
||||||
now: "Dabar",
|
nowShort: "Dabar",
|
||||||
|
nowLong: "Dabar",
|
||||||
minpvShort: "Min",
|
minpvShort: "Min",
|
||||||
minpvLong: "Min + PV",
|
minpvLong: "Min + PV",
|
||||||
pvShort: "PV",
|
pvShort: "PV",
|
||||||
pvLong: "Tik PV",
|
pvLong: "PV",
|
||||||
|
fast: "Greitai",
|
||||||
|
cheap: "Pigiai",
|
||||||
},
|
},
|
||||||
loadpoint: {
|
loadpoint: {
|
||||||
fallbackName: "Įkroviklis",
|
fallbackName: "Įkroviklis",
|
||||||
remoteDisabledSoft: "{source}: adaptyvus PV įkrovimas išjungtas",
|
remoteDisabledSoft: "{source}: adaptyvus PV įkrovimas išjungtas",
|
||||||
remoteDisabledHard: "{source}: Išjungtas",
|
remoteDisabledHard: "{source}: išjungtas",
|
||||||
|
power: "Galia",
|
||||||
|
charged: "Įkrauta",
|
||||||
|
duration: "Trukmė",
|
||||||
|
remaining: "Liko",
|
||||||
},
|
},
|
||||||
|
vehicles: "Autoparkas",
|
||||||
vehicle: {
|
vehicle: {
|
||||||
fallbackName: "Automobilis",
|
fallbackName: "Automobilis",
|
||||||
|
vehicleSoC: "Įkrova",
|
||||||
|
targetSoC: "Limitas",
|
||||||
|
noVehicle: "Nėra automobilio",
|
||||||
},
|
},
|
||||||
vehicleSoC: {
|
vehicleSoC: {
|
||||||
disconnected: "neprijungtas",
|
disconnected: "neprijungtas",
|
||||||
charging: "vyksta įkrovimas",
|
charging: "vyksta įkrovimas",
|
||||||
ready: "leidžiama įkrauti",
|
ready: "leidžiama įkrauti",
|
||||||
connected: "prijungtas",
|
connected: "automobilis prijungtas",
|
||||||
},
|
},
|
||||||
vehicleSubline: {
|
vehicleStatus: {
|
||||||
mincharge: "minimalus įkrovimas iki {soc}%",
|
minCharge: "minimalus įkrovimas iki {soc}%.",
|
||||||
|
waitForVehicle: "Įkrovimas leidžiamas. Laukiama automobilio signalo.",
|
||||||
|
charging: "Įkraunama.",
|
||||||
|
targetChargePlanned: "Suplanuotas įkrovimas, prasidės {start}.",
|
||||||
|
targetChargeWaitForVehicle: "Suplanuotas įkrovimas leidžiamas. Laukiama automobilio signalo.",
|
||||||
|
targetChargeActive: "Suplanuotas įkrovimas aktyvuotas.",
|
||||||
|
connected: "Prijungtas.",
|
||||||
|
pvDisable: "Trūksta saulės, įkrovimo pauzė už {remaining}.",
|
||||||
|
pvEnable: "Saulės užtenka, įkrovimas prasidės už {remaining}.",
|
||||||
|
scale1p: "Sumažinti į vienfazį įkrovimą už {remaining}.",
|
||||||
|
scale3p: "Padidinti į trifazį įkrovimą už {remaining}.",
|
||||||
|
disconnected: "Neprijungtas.",
|
||||||
|
unknown: "",
|
||||||
},
|
},
|
||||||
provider: {
|
provider: {
|
||||||
login: "prisijungti",
|
login: "prisijungti",
|
||||||
logout: "atsijungti",
|
logout: "atsijungti",
|
||||||
},
|
},
|
||||||
targetCharge: {
|
targetCharge: {
|
||||||
inactiveLabel: "Suplanuota įkrovimo pabaiga",
|
title: "Įkrauti iki",
|
||||||
activeLabel: "įkrauti iki {time}",
|
inactiveLabel: "Įkrauti iki",
|
||||||
|
activeLabel: "{time}",
|
||||||
modalTitle: "Nustatyti įkrovimo pabaigos laiką",
|
modalTitle: "Nustatyti įkrovimo pabaigos laiką",
|
||||||
description: "Kada automobilis turėtų būti įkrautas iki <strong>{targetSoC}%</strong>?",
|
setTargetTime: "nenustatytas",
|
||||||
|
description: "Kada automobilis turėtų būti įkrautas iki {targetSoC}%?",
|
||||||
today: "šiandien",
|
today: "šiandien",
|
||||||
tomorrow: "rytoj",
|
tomorrow: "rytoj",
|
||||||
targetIsInThePast: "Pasirinktas laikas yra praeityje.",
|
targetIsInThePast: "Pasirinktas laikas yra praeityje.",
|
||||||
remove: "Panaikinti",
|
remove: "Panaikinti",
|
||||||
activate: "Aktyvuoti",
|
activate: "Aktyvuoti",
|
||||||
experimentalLabel: "eksperimentinis",
|
experimentalLabel: "Eksperimentinis",
|
||||||
experimentalText: `
|
experimentalText: `
|
||||||
Ši funkcija yra ankstyvoje integravimo stadijoje.
|
Ši funkcija veikia, bet dar nėra tobula.
|
||||||
Algoritmas kol kas nėra tobulas.
|
Apie netikėtą elgesį praneškite mūsų
|
||||||
Nustatytas laikas neišsaugojamas - neišlieka po serverio restarto.
|
|
||||||
Dėl šių priežasčių pernelyg nepasitikėkite šia eksperimentine funkcija.
|
|
||||||
Visgi, norėtume gauti atsiliepimų apie jos veikimą bei pasiūlymų tobulinimui mūsų
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
loadpointDetails: {
|
|
||||||
power: "Galia",
|
|
||||||
vehicleRange: "Apytikris nuvažiuojamas atstumas",
|
|
||||||
charged: "Įkrauta",
|
|
||||||
duration: "Įkrovimas vyksta",
|
|
||||||
remaining: "Pabaiga už",
|
|
||||||
tooltip: {
|
|
||||||
phases: {
|
|
||||||
scale1p: "Perjungimas į vienfazį įkrovimą už {remaining}.",
|
|
||||||
scale3p: "Perjungimas į trifazį įkrovimą už {remaining}.",
|
|
||||||
charge1p: "Vienfazis įkrovimas.",
|
|
||||||
charge2p: "Dvifazis įkrovimas.",
|
|
||||||
charge3p: "Trifazis įkrovimas.",
|
|
||||||
},
|
|
||||||
pv: {
|
|
||||||
enable: "Saulės energijos pakanka, įkrovimas tęsiamas už {remaining}.",
|
|
||||||
disable: "Nepakanka saulės energijos, įkrovimo pauzė už {remaining}.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import Vue from "vue";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
|
||||||
|
|
||||||
import { faBatteryEmpty } from "@fortawesome/free-solid-svg-icons/faBatteryEmpty";
|
|
||||||
import { faBatteryFull } from "@fortawesome/free-solid-svg-icons/faBatteryFull";
|
|
||||||
import { faBatteryHalf } from "@fortawesome/free-solid-svg-icons/faBatteryHalf";
|
|
||||||
import { faBatteryQuarter } from "@fortawesome/free-solid-svg-icons/faBatteryQuarter";
|
|
||||||
import { faBatteryThreeQuarters } from "@fortawesome/free-solid-svg-icons/faBatteryThreeQuarters";
|
|
||||||
import { faAngleUp } from "@fortawesome/free-solid-svg-icons/faAngleUp";
|
|
||||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons/faAngleDown";
|
|
||||||
import { faClock } from "@fortawesome/free-solid-svg-icons/faClock";
|
|
||||||
import { faMoon } from "@fortawesome/free-solid-svg-icons/faMoon";
|
|
||||||
import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons/faExclamationTriangle";
|
|
||||||
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
|
|
||||||
import { faSun as farSun } from "@fortawesome/free-regular-svg-icons/faSun";
|
|
||||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle";
|
|
||||||
import { faFlask } from "@fortawesome/free-solid-svg-icons/faFlask";
|
|
||||||
import { faTemperatureHigh } from "@fortawesome/free-solid-svg-icons/faTemperatureHigh";
|
|
||||||
import { faTemperatureLow } from "@fortawesome/free-solid-svg-icons/faTemperatureLow";
|
|
||||||
import { faThermometerHalf } from "@fortawesome/free-solid-svg-icons/faThermometerHalf";
|
|
||||||
import { faHeart as farHeart } from "@fortawesome/free-regular-svg-icons/faHeart";
|
|
||||||
import { faHeart as fasHeart } from "@fortawesome/free-solid-svg-icons/faHeart";
|
|
||||||
import { faGift } from "@fortawesome/free-solid-svg-icons/faGift";
|
|
||||||
import { faBox } from "@fortawesome/free-solid-svg-icons/faBox";
|
|
||||||
import { faHome } from "@fortawesome/free-solid-svg-icons/faHome";
|
|
||||||
import { faWrench } from "@fortawesome/free-solid-svg-icons/faWrench";
|
|
||||||
import { faCar } from "@fortawesome/free-solid-svg-icons/faCar";
|
|
||||||
import { faSquare } from "@fortawesome/free-solid-svg-icons/faSquare";
|
|
||||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
|
|
||||||
import { faAngleDoubleLeft } from "@fortawesome/free-solid-svg-icons/faAngleDoubleLeft";
|
|
||||||
import { faAngleDoubleRight } from "@fortawesome/free-solid-svg-icons/faAngleDoubleRight";
|
|
||||||
|
|
||||||
library.add(
|
|
||||||
faAngleDown,
|
|
||||||
faAngleUp,
|
|
||||||
faBatteryEmpty,
|
|
||||||
faBatteryFull,
|
|
||||||
faBatteryHalf,
|
|
||||||
faBatteryQuarter,
|
|
||||||
faBatteryThreeQuarters,
|
|
||||||
faBox,
|
|
||||||
faCar,
|
|
||||||
faAngleDoubleLeft,
|
|
||||||
faAngleDoubleRight,
|
|
||||||
faClock,
|
|
||||||
faMoon,
|
|
||||||
faExclamationCircle,
|
|
||||||
faExclamationTriangle,
|
|
||||||
faGift,
|
|
||||||
faHome,
|
|
||||||
faWrench,
|
|
||||||
farHeart,
|
|
||||||
fasHeart,
|
|
||||||
faSquare,
|
|
||||||
faSun,
|
|
||||||
farSun,
|
|
||||||
faInfoCircle,
|
|
||||||
faFlask,
|
|
||||||
faTemperatureHigh,
|
|
||||||
faTemperatureLow,
|
|
||||||
faThermometerHalf
|
|
||||||
);
|
|
||||||
|
|
||||||
// eslint-disable-next-line vue/component-definition-name-casing
|
|
||||||
Vue.component("fa-icon", FontAwesomeIcon);
|
|
||||||
|
|
@ -4,7 +4,9 @@ export default {
|
||||||
collectProps: function (component) {
|
collectProps: function (component) {
|
||||||
let data = {};
|
let data = {};
|
||||||
for (var p in component.props) {
|
for (var p in component.props) {
|
||||||
data[p] = this[p];
|
if (p in this) {
|
||||||
|
data[p] = this[p];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -17,16 +17,21 @@ export default {
|
||||||
val = Math.abs(val);
|
val = Math.abs(val);
|
||||||
return val >= this.fmtLimit ? this.round(val / 1e3, this.fmtDigits) : this.round(val, 0);
|
return val >= this.fmtLimit ? this.round(val / 1e3, this.fmtDigits) : this.round(val, 0);
|
||||||
},
|
},
|
||||||
fmtKw: function (watt, kw = true, withUnit = true) {
|
fmtKw: function (watt = 0, kw = true, withUnit = true) {
|
||||||
const digits = kw ? 1 : 0;
|
const digits = kw ? 1 : 0;
|
||||||
const value = kw ? watt / 1000 : watt;
|
const value = kw ? watt / 1000 : watt;
|
||||||
let unit = "";
|
let unit = "";
|
||||||
if (withUnit) {
|
if (withUnit) {
|
||||||
unit = kw ? " kW" : " W";
|
unit = kw ? " kW" : " W";
|
||||||
}
|
}
|
||||||
return (
|
return `${new Intl.NumberFormat(this.$i18n.locale, {
|
||||||
this.$n(value, { minimumFractionDigits: digits, maximumFractionDigits: digits }) + unit
|
style: "decimal",
|
||||||
);
|
minimumFractionDigits: digits,
|
||||||
|
maximumFractionDigits: digits,
|
||||||
|
}).format(value)}${unit}`;
|
||||||
|
},
|
||||||
|
fmtKWh: function (val) {
|
||||||
|
return this.fmtKw(val) + "h";
|
||||||
},
|
},
|
||||||
fmtUnit: function (val) {
|
fmtUnit: function (val) {
|
||||||
return Math.abs(val) >= this.fmtLimit ? "k" : "";
|
return Math.abs(val) >= this.fmtLimit ? "k" : "";
|
||||||
|
|
@ -88,15 +93,42 @@ export default {
|
||||||
const mm = `${date.getMinutes()}`.padStart(2, "0");
|
const mm = `${date.getMinutes()}`.padStart(2, "0");
|
||||||
return `${HH}:${mm}`;
|
return `${HH}:${mm}`;
|
||||||
},
|
},
|
||||||
fmtAbsoluteDate: function (date) {
|
isToday: function (date) {
|
||||||
|
const today = new Date();
|
||||||
|
return today.toDateString() === date.toDateString();
|
||||||
|
},
|
||||||
|
isTomorrow: function (date) {
|
||||||
|
const tomorrow = new Date();
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
return tomorrow.toDateString() === date.toDateString();
|
||||||
|
},
|
||||||
|
weekdayPrefix: function (date) {
|
||||||
|
const rtf = new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: "auto" });
|
||||||
|
|
||||||
|
if (this.isToday(date)) {
|
||||||
|
return rtf.formatToParts(0, "day")[0].value;
|
||||||
|
}
|
||||||
|
if (this.isTomorrow(date)) {
|
||||||
|
return rtf.formatToParts(1, "day")[0].value;
|
||||||
|
}
|
||||||
return new Intl.DateTimeFormat(this.$i18n.locale, {
|
return new Intl.DateTimeFormat(this.$i18n.locale, {
|
||||||
weekday: "short",
|
weekday: "short",
|
||||||
|
}).format(date);
|
||||||
|
},
|
||||||
|
fmtAbsoluteDate: function (date) {
|
||||||
|
const weekday = this.weekdayPrefix(date);
|
||||||
|
const hour = new Intl.DateTimeFormat(this.$i18n.locale, {
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "numeric",
|
minute: "numeric",
|
||||||
}).format(date);
|
}).format(date);
|
||||||
|
|
||||||
|
return `${weekday} ${hour}`;
|
||||||
},
|
},
|
||||||
fmtMoney: function (amout = 0, currency = "EUR") {
|
fmtMoney: function (amout = 0, currency = "EUR") {
|
||||||
return this.$n(amout, { style: "currency", currency });
|
return new Intl.NumberFormat(this.$i18n.locale, {
|
||||||
|
style: "currency",
|
||||||
|
currency,
|
||||||
|
}).format(amout);
|
||||||
},
|
},
|
||||||
fmtPricePerKWh: function (amout = 0, currency = "EUR") {
|
fmtPricePerKWh: function (amout = 0, currency = "EUR") {
|
||||||
let unit = currency;
|
let unit = currency;
|
||||||
|
|
@ -107,7 +139,10 @@ export default {
|
||||||
unit = "ct";
|
unit = "ct";
|
||||||
maximumFractionDigits = 1;
|
maximumFractionDigits = 1;
|
||||||
}
|
}
|
||||||
return `${this.$n(value, { style: "decimal", maximumFractionDigits })} ${unit}/kWh`;
|
return `${new Intl.NumberFormat(this.$i18n.locale, {
|
||||||
|
style: "decimal",
|
||||||
|
maximumFractionDigits,
|
||||||
|
}).format(value)} ${unit}/kWh`;
|
||||||
},
|
},
|
||||||
fmtTimeAgo: function (elapsed) {
|
fmtTimeAgo: function (elapsed) {
|
||||||
const units = {
|
const units = {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,8 @@
|
||||||
import VueRouter from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import Vue from "vue";
|
|
||||||
import Main from "./views/Main";
|
|
||||||
import Config from "./views/Config";
|
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
import Main from "./views/Main.vue";
|
||||||
|
|
||||||
const routes = [
|
export default createRouter({
|
||||||
{ path: "/", component: Main },
|
history: createWebHistory(),
|
||||||
{ path: "/config", component: Config },
|
routes: [{ path: "/", component: Main, props: true }],
|
||||||
];
|
|
||||||
|
|
||||||
export default new VueRouter({
|
|
||||||
routes,
|
|
||||||
linkExactActiveClass: "active", // Bootstrap <nav>
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import Vue from "vue";
|
import { reactive } from "vue";
|
||||||
|
|
||||||
function setProperty(obj, props, value) {
|
function setProperty(obj, props, value) {
|
||||||
const prop = props.shift();
|
const prop = props.shift();
|
||||||
if (!obj[prop]) {
|
if (!obj[prop]) {
|
||||||
Vue.set(obj, prop, {});
|
obj[prop] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.length) {
|
if (!props.length) {
|
||||||
|
|
@ -18,16 +18,18 @@ function setProperty(obj, props, value) {
|
||||||
setProperty(obj[prop], props, value);
|
setProperty(obj[prop], props, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
loadpoints: [], // ensure array type
|
||||||
|
});
|
||||||
|
|
||||||
const store = {
|
const store = {
|
||||||
state: {
|
state,
|
||||||
loadpoints: [], // ensure array type
|
|
||||||
},
|
|
||||||
update: function (msg) {
|
update: function (msg) {
|
||||||
Object.keys(msg).forEach(function (k) {
|
Object.keys(msg).forEach(function (k) {
|
||||||
if (typeof window.app[k] === "function") {
|
if (typeof window.app[k] === "function") {
|
||||||
window.app[k]({ message: msg[k] });
|
window.app[k]({ message: msg[k] });
|
||||||
} else {
|
} else {
|
||||||
setProperty(store.state, k.split("."), msg[k]);
|
setProperty(state, k.split("."), msg[k]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import "v-tooltip/dist/v-tooltip.css";
|
|
||||||
import Vue from "vue";
|
|
||||||
import VTooltip from "v-tooltip";
|
|
||||||
VTooltip.options.offset = [0, 10];
|
|
||||||
VTooltip.options.themes.tooltip.triggers = ["click", "focus"];
|
|
||||||
|
|
||||||
Vue.use(VTooltip);
|
|
||||||
|
|
@ -1,171 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="app d-flex flex-column justify-content-between overflow-hidden">
|
<div class="app overflow-hidden">
|
||||||
<div class="flex-grow-1 d-flex flex-column justify-content-between">
|
<metainfo>
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
<template #title="{ content }">{{ content ? `${content} | evcc` : `evcc` }}</template>
|
||||||
<div class="container">
|
</metainfo>
|
||||||
<a class="navbar-brand" href="https://evcc.io/" target="_blank">
|
<router-view :notifications="notifications"></router-view>
|
||||||
<Logo class="logo"></Logo>
|
|
||||||
</a>
|
|
||||||
<div class="d-flex flex-grow-1 justify-content-end">
|
|
||||||
<Notifications :notifications="notifications" />
|
|
||||||
<button
|
|
||||||
class="navbar-toggler"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#navbarNavAltMarkup"
|
|
||||||
aria-controls="navbarNavAltMarkup"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-label="Toggle navigation"
|
|
||||||
>
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="navbarNavAltMarkup" class="collapse navbar-collapse flex-grow-0">
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
<li v-if="providerLogins.length > 0" class="nav-item dropdown">
|
|
||||||
<a
|
|
||||||
class="nav-link dropdown-toggle"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
href="#"
|
|
||||||
role="button"
|
|
||||||
aria-expanded="false"
|
|
||||||
>{{ $t("header.login") }}
|
|
||||||
<span v-if="logoutCount > 0" class="badge bg-secondary">{{
|
|
||||||
logoutCount
|
|
||||||
}}</span>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li
|
|
||||||
v-for="login in providerLogins"
|
|
||||||
:key="login.title"
|
|
||||||
class="dropdown-item"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="dropdown-item"
|
|
||||||
type="button"
|
|
||||||
@click="handleProviderAuthorization(login)"
|
|
||||||
>
|
|
||||||
{{ login.title }}
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
login.loggedIn
|
|
||||||
? "main.provider.logout"
|
|
||||||
: "main.provider.login"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a
|
|
||||||
class="nav-link"
|
|
||||||
href="https://docs.evcc.io/blog/"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{ $t("header.blog") }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a
|
|
||||||
class="nav-link"
|
|
||||||
href="https://docs.evcc.io/docs/Home/"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{ $t("header.docs") }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a
|
|
||||||
class="nav-link"
|
|
||||||
href="https://github.com/evcc-io/evcc"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{ $t("header.github") }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<router-view
|
|
||||||
class="flex-grow-1 d-flex flex-column justify-content-stretch"
|
|
||||||
></router-view>
|
|
||||||
</div>
|
|
||||||
<Footer :version="version" :sponsor="sponsor" :savings="savings"></Footer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import "../icons";
|
|
||||||
import Logo from "../components/Logo";
|
|
||||||
import Footer from "../components/Footer";
|
|
||||||
import Notifications from "../components/Notifications";
|
|
||||||
|
|
||||||
import store from "../store";
|
import store from "../store";
|
||||||
|
|
||||||
import baseAPI from "../baseapi";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
components: { Logo, Footer, Notifications },
|
|
||||||
props: {
|
props: {
|
||||||
notifications: Array,
|
notifications: Array,
|
||||||
},
|
},
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
compact: false,
|
|
||||||
store: this.$root.$data.store,
|
|
||||||
installedVersion: window.evcc.version,
|
|
||||||
commit: window.evcc.commit,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
version: function () {
|
|
||||||
return {
|
|
||||||
installed: this.installedVersion,
|
|
||||||
commit: this.commit,
|
|
||||||
available: this.store.state.availableVersion,
|
|
||||||
releaseNotes: this.store.state.releaseNotes,
|
|
||||||
hasUpdater: this.store.state.hasUpdater,
|
|
||||||
uploadMessage: this.store.state.uploadMessage,
|
|
||||||
uploadProgress: this.store.state.uploadProgress,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
title: function () {
|
|
||||||
return this.store.state.siteTitle;
|
|
||||||
},
|
|
||||||
sponsor: function () {
|
|
||||||
return this.store.state.sponsor;
|
|
||||||
},
|
|
||||||
savings: function () {
|
|
||||||
return {
|
|
||||||
since: this.store.state.savingsSince,
|
|
||||||
totalCharged: this.store.state.savingsTotalCharged,
|
|
||||||
gridCharged: this.store.state.savingsGridCharged,
|
|
||||||
selfConsumptionCharged: this.store.state.savingsSelfConsumptionCharged,
|
|
||||||
amount: this.store.state.savingsAmount,
|
|
||||||
effectivePrice: this.store.state.savingsEffectivePrice,
|
|
||||||
selfConsumptionPercent: this.store.state.savingsSelfConsumptionPercent,
|
|
||||||
gridPrice: this.store.state.tariffGrid,
|
|
||||||
feedInPrice: this.store.state.tariffFeedIn,
|
|
||||||
currency: this.store.state.currency,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
logoutCount() {
|
|
||||||
return this.providerLogins.filter((login) => !login.loggedIn).length;
|
|
||||||
},
|
|
||||||
providerLogins() {
|
|
||||||
return this.store.state.auth
|
|
||||||
? Object.entries(this.store.state.auth.vehicles).map(([k, v]) => ({
|
|
||||||
title: k,
|
|
||||||
loggedIn: v.authenticated,
|
|
||||||
loginPath: v.uri + "/login",
|
|
||||||
logoutPath: v.uri + "/logout",
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
created: function () {
|
created: function () {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
this.compact = urlParams.get("compact");
|
this.compact = urlParams.get("compact");
|
||||||
|
|
@ -211,32 +60,15 @@ export default {
|
||||||
reload() {
|
reload() {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
},
|
},
|
||||||
handleProviderAuthorization: async function (provider) {
|
|
||||||
if (!provider.loggedIn) {
|
|
||||||
baseAPI.post(provider.loginPath).then(function (response) {
|
|
||||||
window.location.href = response.data.loginUri;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
baseAPI.post(provider.logoutPath);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
metaInfo() {
|
metaInfo() {
|
||||||
return {
|
return { title: store.state.siteTitle || "" };
|
||||||
title: this.title ? `evcc | ${this.title}` : "evcc",
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.logo {
|
|
||||||
width: 85px;
|
|
||||||
}
|
|
||||||
.app {
|
.app {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
background-color: white;
|
||||||
.title {
|
|
||||||
position: relative;
|
|
||||||
top: 0.1rem;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div>
|
||||||
<Site v-if="configured" v-bind="state"></Site>
|
<Site v-if="configured" :notifications="notifications" v-bind="state"></Site>
|
||||||
<div v-else>
|
<div v-else class="container">
|
||||||
<div class="row py-5">
|
<div class="row py-5">
|
||||||
<div class="col12">
|
<div class="col12">
|
||||||
<p class="h1 pt-5 pb-2 border-bottom">Willkommen bei evcc</p>
|
<p class="h1 pt-5 pb-2 border-bottom">Willkommen bei evcc</p>
|
||||||
|
|
@ -80,13 +80,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Site from "../components/Site";
|
import Site from "../components/Site.vue";
|
||||||
|
import store from "../store";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Main",
|
name: "Main",
|
||||||
components: { Site },
|
components: { Site },
|
||||||
|
props: {
|
||||||
|
notifications: Array,
|
||||||
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return this.$root.$data.store; // global state
|
return store;
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
configured: function () {
|
configured: function () {
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
11
assets/public/meta/browserconfig.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square70x70logo src="meta/mstile-70x70.png" />
|
||||||
|
<square150x150logo src="meta/mstile-150x150.png" />
|
||||||
|
<square310x310logo src="meta/mstile-310x310.png" />
|
||||||
|
<TileColor>#18191a</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
|
Before Width: | Height: | Size: 646 B After Width: | Height: | Size: 646 B |
|
Before Width: | Height: | Size: 797 B After Width: | Height: | Size: 797 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
200
cmd/demo.yaml
|
|
@ -9,16 +9,23 @@ javascript:
|
||||||
- vm: shared
|
- vm: shared
|
||||||
script: |
|
script: |
|
||||||
state = {
|
state = {
|
||||||
residualpower: 300,
|
residualpower: 500,
|
||||||
batterypower: 0,
|
pvpower: 5000,
|
||||||
pvpower: 6200,
|
batterypower: -750,
|
||||||
gridpower: 0,
|
batterySoC: 55,
|
||||||
chargepower: 0,
|
gridpower: -1000,
|
||||||
maxcurrent: 0,
|
loadpoints: [
|
||||||
battery: 63, // car
|
{ enabled: true, vehicleSoC: 62, maxcurrent: 6, phases: 1, chargepower: 0 },
|
||||||
|
{ enabled: false, vehicleSoC: 22, maxcurrent: 0, phases: 3, chargepower: 0 }
|
||||||
|
]
|
||||||
};
|
};
|
||||||
function log() { console.log("state:", JSON.stringify(state)); }
|
function get() {
|
||||||
function set() { console.log("set:", param+"="+val); }
|
console.log("state:", JSON.stringify(state));
|
||||||
|
}
|
||||||
|
function set() {
|
||||||
|
console.log(param+":", val);
|
||||||
|
console.log("state:", JSON.stringify(state));
|
||||||
|
}
|
||||||
|
|
||||||
meters:
|
meters:
|
||||||
- name: grid
|
- name: grid
|
||||||
|
|
@ -27,22 +34,9 @@ meters:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
script: |
|
script: |
|
||||||
state.pvpower += 200*(Math.random()-0.5);
|
state.pvpower = 8000+500*Math.random();
|
||||||
state.chargepower = 0;
|
state.batterypower = state.gridpower > 0 ? 1000 * Math.random() : 0;
|
||||||
if (state.enabled) {
|
state.gridpower = state.loadpoints[0].chargepower + state.loadpoints[1].chargepower + state.residualpower - state.batterypower - state.pvpower;
|
||||||
state.chargepower = state.maxcurrent * 3 * 230;
|
|
||||||
state.batterypower = 0;
|
|
||||||
}
|
|
||||||
state.gridpower = -state.pvpower + state.chargepower + state.residualpower + state.batterypower;
|
|
||||||
// if (state.gridpower < 0) {
|
|
||||||
// state.batterypower = -state.gridpower;
|
|
||||||
// if (state.batterypower > 1000) {
|
|
||||||
// state.batterypower = 1000;
|
|
||||||
// }
|
|
||||||
// state.gridpower += state.batterypower;
|
|
||||||
// } else {
|
|
||||||
// state.batterypower = 0;
|
|
||||||
// }
|
|
||||||
state.gridpower;
|
state.gridpower;
|
||||||
|
|
||||||
- name: pv
|
- name: pv
|
||||||
|
|
@ -50,7 +44,7 @@ meters:
|
||||||
power:
|
power:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
script: state.pvpower
|
script: state.pvpower;
|
||||||
|
|
||||||
- name: battery
|
- name: battery
|
||||||
type: custom
|
type: custom
|
||||||
|
|
@ -61,56 +55,148 @@ meters:
|
||||||
soc:
|
soc:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
script: "30"
|
script: |
|
||||||
- name: charge
|
if (state.batterypower < 0) state.batterySoC++; else state.batterySoC--;
|
||||||
|
if (state.batterySoC < 10) state.batterySoC = 90;
|
||||||
|
if (state.batterySoC > 90) state.batterySoC = 10;
|
||||||
|
state.batterySoC;
|
||||||
|
- name: meter_charger_1
|
||||||
type: custom
|
type: custom
|
||||||
power:
|
power:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
script: state.chargepower;
|
script: state.loadpoints[0].chargepower;
|
||||||
soc:
|
- name: meter_charger_2
|
||||||
|
type: custom
|
||||||
|
power:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
script: "30"
|
script: state.loadpoints[1].chargepower;
|
||||||
|
- name: meter_charger_3
|
||||||
|
type: custom
|
||||||
|
power:
|
||||||
|
source: js
|
||||||
|
script: 0
|
||||||
chargers:
|
chargers:
|
||||||
- name: demo
|
- name: charger_1
|
||||||
type: custom
|
type: custom
|
||||||
enable:
|
enable:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
script: state.enabled = val;
|
script: |
|
||||||
|
set();
|
||||||
|
var lp = state.loadpoints[0];
|
||||||
|
lp.enabled = val;
|
||||||
|
if (lp.enabled) lp.chargepower = lp.maxcurrent * 230 * lp.phases; else lp.chargepower = 0;
|
||||||
enabled:
|
enabled:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
script: |
|
script: |
|
||||||
state.enabled;
|
state.loadpoints[0].enabled;
|
||||||
status:
|
status:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
script: |
|
script: |
|
||||||
if (state.enabled) "C"; else "B";
|
if (state.loadpoints[0].enabled) "C"; else "B";
|
||||||
maxcurrent:
|
maxcurrent:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
script: state.maxcurrent = val;
|
script: |
|
||||||
|
set();
|
||||||
|
var lp = state.loadpoints[0];
|
||||||
|
lp.maxcurrent = val;
|
||||||
|
if (lp.enabled) lp.chargepower = lp.maxcurrent * 230 * lp.phases; else lp.chargepower = 0;
|
||||||
|
- name: charger_2
|
||||||
|
type: custom
|
||||||
|
enable:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: |
|
||||||
|
set();
|
||||||
|
var lp = state.loadpoints[1];
|
||||||
|
lp.enabled = val;
|
||||||
|
if (lp.enabled) lp.chargepower = lp.maxcurrent * 230 * lp.phases; else lp.chargepower = 0;
|
||||||
|
enabled:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: |
|
||||||
|
state.loadpoints[1].enabled;
|
||||||
|
status:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: |
|
||||||
|
if (state.loadpoints[1].enabled) "C"; else "B";
|
||||||
|
maxcurrent:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: |
|
||||||
|
set();
|
||||||
|
var lp = state.loadpoints[1];
|
||||||
|
lp.maxcurrent = val;
|
||||||
|
if (lp.enabled) lp.chargepower = lp.maxcurrent * 230 * lp.phases; else lp.chargepower = 0;
|
||||||
|
- name: charger_3
|
||||||
|
type: custom
|
||||||
|
enable:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script:
|
||||||
|
enabled:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: false
|
||||||
|
status:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: "'A'"
|
||||||
|
maxcurrent:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: 16
|
||||||
|
|
||||||
vehicles:
|
vehicles:
|
||||||
- name: demo
|
- name: vehicle_1
|
||||||
title: e-Golf
|
title: blauer e-Golf
|
||||||
type: custom
|
type: custom
|
||||||
capacity: 50
|
|
||||||
soc:
|
soc:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
script: |
|
script: |
|
||||||
if (state.chargepower > 0) state.battery++; else state.battery--;
|
var lp = state.loadpoints[0];
|
||||||
if (state.battery < 15) state.battery = 15;
|
if (lp.chargepower > 0) lp.vehicleSoC++; else lp.vehicleSoC--;
|
||||||
if (state.battery > 100) state.battery = 100;
|
if (lp.vehicleSoC < 15) lp.vehicleSoC = 80;
|
||||||
state.battery;
|
if (lp.vehicleSoC > 80) lp.vehicleSoC = 15;
|
||||||
|
lp.vehicleSoC;
|
||||||
|
range:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: |
|
||||||
|
var lp = state.loadpoints[0]
|
||||||
|
var range = (44 * lp.vehicleSoC) / 15;
|
||||||
|
range
|
||||||
|
capacity: 44
|
||||||
|
- name: vehicle_2
|
||||||
|
title: weißes Model 3
|
||||||
|
type: custom
|
||||||
|
soc:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: |
|
||||||
|
var lp = state.loadpoints[1];
|
||||||
|
if (lp.chargepower > 0) lp.vehicleSoC++; else lp.vehicleSoC--;
|
||||||
|
if (lp.vehicleSoC < 15) lp.vehicleSoC = 75;
|
||||||
|
if (lp.vehicleSoC > 75) lp.vehicleSoC = 15;
|
||||||
|
lp.vehicleSoC;
|
||||||
|
range:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: |
|
||||||
|
var lp = state.loadpoints[1]
|
||||||
|
var range = (80 * lp.vehicleSoC) / 17;
|
||||||
|
range
|
||||||
|
capacity: 80
|
||||||
|
|
||||||
site:
|
site:
|
||||||
title: Demo
|
title: Zuhause
|
||||||
meters:
|
meters:
|
||||||
grid: grid
|
grid: grid
|
||||||
pv: pv
|
pv: pv
|
||||||
|
|
@ -118,6 +204,24 @@ site:
|
||||||
|
|
||||||
loadpoints:
|
loadpoints:
|
||||||
- title: Carport
|
- title: Carport
|
||||||
charger: demo
|
charger: charger_1
|
||||||
meter: charge
|
mode: pv
|
||||||
vehicle: demo
|
phases: 1
|
||||||
|
soc:
|
||||||
|
target: 90
|
||||||
|
meters:
|
||||||
|
charge: meter_charger_1
|
||||||
|
vehicle: vehicle_1
|
||||||
|
- title: Garage
|
||||||
|
charger: charger_2
|
||||||
|
soc:
|
||||||
|
target: 75
|
||||||
|
mode: "off"
|
||||||
|
meters:
|
||||||
|
charge: meter_charger_2
|
||||||
|
vehicle: vehicle_2
|
||||||
|
# - title: Garage rechts
|
||||||
|
# charger: charger_3
|
||||||
|
# mode: "off"
|
||||||
|
# meters:
|
||||||
|
# charge: meter_charger_3
|
||||||
|
|
|
||||||
BIN
dist/assets/Montserrat-Bold.294653dc.woff2
vendored
Normal file
BIN
dist/assets/Montserrat-Medium.f8c1310c.woff2
vendored
Normal file
6
dist/assets/index.5a1f45bf.css
vendored
Normal file
93
dist/assets/index.a45f25da.js
vendored
Normal file
6
dist/css/chunk-vendors.38c609a9.css
vendored
1
dist/css/index.0a6b5a0e.css
vendored
11
dist/ico/browserconfig.xml
vendored
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square70x70logo src="ico/mstile-70x70.png" />
|
|
||||||
<square150x150logo src="ico/mstile-150x150.png" />
|
|
||||||
<square310x310logo src="ico/mstile-310x310.png" />
|
|
||||||
<TileColor>#18191a</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
||||||
37
dist/index.html
vendored
|
|
@ -1,5 +1,38 @@
|
||||||
<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><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?[[.Version]]"><link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png?[[.Version]]"><link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png?[[.Version]]"><link rel="manifest" href="ico/site.webmanifest"><link rel="mask-icon" href="ico/safari-pinned-tab.svg?[[.Version]]" color="#18191a"><link rel="shortcut icon" href="ico/favicon.ico?[[.Version]]"><meta name="apple-mobile-web-app-title" content="evcc"><meta name="application-name" content="evcc"><meta name="msapplication-TileColor" content="#18191a"><meta name="msapplication-config" content="ico/browserconfig.xml"><meta name="theme-color" content="#18191a"><title>evcc</title><link href="css/chunk-vendors.38c609a9.css" rel="preload" as="style"><link href="css/index.0a6b5a0e.css" rel="preload" as="style"><link href="js/chunk-vendors.b6901fd4.js" rel="preload" as="script"><link href="js/index.f02d5732.js" rel="preload" as="script"><link href="css/chunk-vendors.38c609a9.css" rel="stylesheet"><link href="css/index.0a6b5a0e.css" rel="stylesheet"></head><body><script>window.evcc = {
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content="evcc - Sonne tanken ☀️🚘" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="evcc" />
|
||||||
|
<meta name="application-name" content="evcc" />
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="meta/apple-touch-icon.png?[[.Version]]" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="meta/favicon-32x32.png?[[.Version]]" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="meta/favicon-16x16.png?[[.Version]]" />
|
||||||
|
<link rel="manifest" href="meta/site.webmanifest?[[.Version]]" />
|
||||||
|
<link rel="mask-icon" href="meta/safari-pinned-tab.svg?[[.Version]]" color="#28293e" />
|
||||||
|
<link rel="shortcut icon" href="meta/favicon.ico?[[.Version]]" />
|
||||||
|
<meta name="msapplication-TileColor" content="#28293e" />
|
||||||
|
<meta name="msapplication-config" content="meta/browserconfig.xml?[[.Version]]" />
|
||||||
|
<meta name="theme-color" content="#28293e" />
|
||||||
|
|
||||||
|
<title>evcc</title>
|
||||||
|
<script type="module" crossorigin src="./assets/index.a45f25da.js"></script>
|
||||||
|
<link rel="stylesheet" href="./assets/index.5a1f45bf.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
window.evcc = {
|
||||||
version: "[[.Version]]",
|
version: "[[.Version]]",
|
||||||
configured: "[[.Configured]]",
|
configured: "[[.Configured]]",
|
||||||
commit: "[[.Commit]]",
|
commit: "[[.Commit]]",
|
||||||
};</script><div id="app"></div><script src="js/chunk-vendors.b6901fd4.js"></script><script src="js/index.f02d5732.js"></script></body></html>
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
30
dist/js/chunk-vendors.b6901fd4.js
vendored
1
dist/js/chunk-vendors.b6901fd4.js.map
vendored
2
dist/js/index.f02d5732.js
vendored
1
dist/js/index.f02d5732.js.map
vendored
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |