Visualisation of Site Energy Flow (#1148)
added energy flow visualisation
This commit is contained in:
parent
383d450637
commit
bb910a5cd5
33 changed files with 6906 additions and 319 deletions
7
Makefile
7
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
.PHONY: default all clean install install-ui ui assets lint lint-ui test build test-release release
|
||||
.PHONY: default all clean install install-ui ui assets lint test-ui lint-ui test build test-release release
|
||||
.PHONY: docker publish-testing publish-latest publish-images
|
||||
.PHONY: prepare-image image-rootfs image-update
|
||||
.PHONY: soc stamps
|
||||
|
|
@ -24,7 +24,7 @@ IMAGE_OPTIONS := -hostname evcc -http_port 8080 github.com/gokrazy/serial-busybo
|
|||
|
||||
default: build
|
||||
|
||||
all: clean install install-ui ui assets lint lint-ui test build
|
||||
all: clean install install-ui ui assets lint test-ui lint-ui test build
|
||||
|
||||
clean:
|
||||
rm -rf dist/
|
||||
|
|
@ -47,6 +47,9 @@ lint:
|
|||
lint-ui:
|
||||
npm run lint
|
||||
|
||||
test-ui:
|
||||
npm run test
|
||||
|
||||
test:
|
||||
@echo "Running testsuite"
|
||||
go test ./...
|
||||
|
|
|
|||
|
|
@ -1,29 +1,23 @@
|
|||
:root {
|
||||
--evcc-green: #66d85a;
|
||||
--evcc-dark-green: #3aba2c;
|
||||
--evcc-yellow: #ffe000;
|
||||
|
||||
--evcc-grid: var(--bs-gray-dark);
|
||||
--evcc-self: var(--evcc-dark-green);
|
||||
--evcc-export: var(--evcc-yellow);
|
||||
|
||||
--bs-primary: var(--evcc-dark-green);
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--evcc-dark-green) !important;
|
||||
}
|
||||
|
||||
/* reverse loading animation */
|
||||
.progress-bar-animated {
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.btn.caption {
|
||||
opacity: 1;
|
||||
}
|
||||
.btn.first {
|
||||
border-top-left-radius: 0.2rem;
|
||||
border-bottom-left-radius: 0.2rem;
|
||||
}
|
||||
code {
|
||||
font-size: 87.5%;
|
||||
color: #e83e8c;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.value,
|
||||
input[type="radio"],
|
||||
label.btn {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.text-muted a,
|
||||
.text-muted a:hover {
|
||||
color: #6c757d !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
.bg-muted {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import "../css/app.css";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "bootstrap";
|
||||
import "../css/app.css";
|
||||
import Vue from "vue";
|
||||
import VueMeta from "vue-meta";
|
||||
import axios from "axios";
|
||||
import App from "./views/App";
|
||||
import router from "./router";
|
||||
import i18n from "./i18n";
|
||||
import store from "./store";
|
||||
|
||||
Vue.use(VueMeta);
|
||||
|
||||
const loc = window.location;
|
||||
axios.defaults.baseURL =
|
||||
loc.protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") + loc.pathname + "api";
|
||||
|
|
|
|||
58
assets/js/components/Energyflow/BatteryIcon.vue
Normal file
58
assets/js/components/Energyflow/BatteryIcon.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div :class="{ 'power--in': charge, 'power--out': discharge }">
|
||||
<fa-icon class="battery" :icon="batteryIcon"></fa-icon
|
||||
><fa-icon class="arrow" icon="angle-double-right"></fa-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "../../icons";
|
||||
|
||||
export default {
|
||||
name: "BatteryIcon",
|
||||
props: {
|
||||
discharge: { type: Boolean },
|
||||
charge: { type: Boolean },
|
||||
soc: { type: Number, default: 0 },
|
||||
},
|
||||
computed: {
|
||||
batteryIcon: function () {
|
||||
if (this.soc > 80) return "battery-full";
|
||||
if (this.soc > 60) return "battery-three-quarters";
|
||||
if (this.soc > 40) return "battery-half";
|
||||
if (this.soc > 20) return "battery-quarter";
|
||||
return "battery-empty";
|
||||
},
|
||||
},
|
||||
};
|
||||
</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>
|
||||
162
assets/js/components/Energyflow/Energyflow.spec.js
Normal file
162
assets/js/components/Energyflow/Energyflow.spec.js
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/* globals describe, it, expect */
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import Energyflow from "./Energyflow.vue";
|
||||
|
||||
describe("Energyflow.vue", () => {
|
||||
const defaultProps = {
|
||||
gridConfigured: true,
|
||||
gridPower: 0,
|
||||
pvConfigured: true,
|
||||
pvPower: 0,
|
||||
batteryConfigured: false,
|
||||
batteryPower: 0,
|
||||
batterySoC: 0,
|
||||
};
|
||||
|
||||
it("using pv and grid power", async () => {
|
||||
const wrapper = shallowMount(Energyflow, {
|
||||
mocks: { $t: (x) => x },
|
||||
propsData: { ...defaultProps, gridPower: 1000, pvPower: 4000 },
|
||||
});
|
||||
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-house-consumption]").text()).toMatch("5.0 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: { $t: (x) => x },
|
||||
propsData: { ...defaultProps, gridPower: -4000, pvPower: 4000 },
|
||||
});
|
||||
|
||||
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-house-consumption]").text()).toMatch("0.0 kW");
|
||||
expect(wrapper.find("[data-test-pv-production]").text()).toMatch("4.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: { $t: (x) => x },
|
||||
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-house-consumption]").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("only grid usage, no pv, idleBattery", async () => {
|
||||
const wrapper = shallowMount(Energyflow, {
|
||||
mocks: { $t: (x) => x },
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
gridPower: 360,
|
||||
pvPower: 0,
|
||||
batteryConfigured: true,
|
||||
batteryPower: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find(".energyflow").trigger("click");
|
||||
|
||||
expect(wrapper.find("[data-test-grid-import]").text()).toMatch("0.4 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-house-consumption]").text()).toMatch("0.4 kW");
|
||||
expect(wrapper.find("[data-test-pv-production]").text()).toMatch("0.0 kW");
|
||||
expect(wrapper.find("[data-test-battery]").text()).toMatch("main.energyflow.battery");
|
||||
});
|
||||
|
||||
it("grid and battery usage, no pv", async () => {
|
||||
const wrapper = shallowMount(Energyflow, {
|
||||
mocks: { $t: (x) => x },
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
gridPower: 300,
|
||||
batteryConfigured: true,
|
||||
batteryPower: 200,
|
||||
batterySoC: 77,
|
||||
pvPower: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find(".energyflow").trigger("click");
|
||||
|
||||
expect(wrapper.find("[data-test-grid-import]").text()).toMatch("0.3 kW");
|
||||
expect(wrapper.find("[data-test-self-consumption]").text()).toMatch("0.2 kW");
|
||||
expect(wrapper.find("[data-test-pv-export]").text()).toMatch("0.0 kW");
|
||||
|
||||
expect(wrapper.find("[data-test-house-consumption]").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("0.2 kW");
|
||||
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: { $t: (x) => x },
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
gridPower: -2500,
|
||||
batteryConfigured: true,
|
||||
batteryPower: -1700,
|
||||
pvPower: 9000,
|
||||
},
|
||||
});
|
||||
|
||||
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-house-consumption]").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: { $t: (x) => x },
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
gridPower: 5555,
|
||||
batteryConfigured: true,
|
||||
batteryPower: 1234,
|
||||
pvPower: 378,
|
||||
},
|
||||
});
|
||||
|
||||
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-house-consumption]").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");
|
||||
});
|
||||
});
|
||||
102
assets/js/components/Energyflow/Energyflow.stories.js
Normal file
102
assets/js/components/Energyflow/Energyflow.stories.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import Energyflow from "./";
|
||||
import i18n from "../../i18n";
|
||||
|
||||
export default {
|
||||
title: "Main/Energyflow",
|
||||
component: Energyflow,
|
||||
argTypes: {
|
||||
gridPower: { control: { type: "range", min: -5000, max: 20000, step: 100 } },
|
||||
pvPower: { control: { type: "range", min: 0, max: 10000, step: 100 } },
|
||||
batteryPower: { control: { type: "range", min: -4000, max: 4000, step: 100 } },
|
||||
batterySoC: { control: { type: "range", min: 0, max: 100, step: 1 } },
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
i18n,
|
||||
props: Object.keys(argTypes),
|
||||
components: { Energyflow },
|
||||
template: '<Energyflow v-bind="$props"></Energyflow>',
|
||||
});
|
||||
|
||||
export const GridAndPV = Template.bind({});
|
||||
GridAndPV.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
gridPower: -2300,
|
||||
pvPower: 7320,
|
||||
batteryConfigured: false,
|
||||
siteTitle: "Home",
|
||||
};
|
||||
|
||||
export const BatteryAndGrid = Template.bind({});
|
||||
BatteryAndGrid.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
gridPower: 1200,
|
||||
pvPower: 0,
|
||||
batteryPower: 800,
|
||||
batterySoC: 77,
|
||||
siteTitle: "Home",
|
||||
};
|
||||
|
||||
export const BatteryCharging = Template.bind({});
|
||||
BatteryCharging.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
gridPower: -1300,
|
||||
pvPower: 5000,
|
||||
batteryPower: -1500,
|
||||
batterySoC: 75,
|
||||
siteTitle: "Home",
|
||||
};
|
||||
|
||||
export const GridPvAndBattery = Template.bind({});
|
||||
GridPvAndBattery.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
gridPower: 700,
|
||||
pvPower: 1000,
|
||||
batteryPower: 1500,
|
||||
batterySoC: 30,
|
||||
siteTitle: "Home",
|
||||
};
|
||||
|
||||
export const BatteryThresholds = Template.bind({});
|
||||
BatteryThresholds.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
gridPower: -510,
|
||||
pvPower: 8740,
|
||||
batteryPower: -100,
|
||||
batterySoC: 95,
|
||||
siteTitle: "Home",
|
||||
};
|
||||
|
||||
export const PvThresholds = Template.bind({});
|
||||
PvThresholds.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
batteryPower: 800,
|
||||
gridPower: 5555,
|
||||
pvPower: 300,
|
||||
batterySoC: 76,
|
||||
siteTitle: "Home",
|
||||
};
|
||||
|
||||
export const GridOnly = Template.bind({});
|
||||
GridOnly.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: false,
|
||||
batteryConfigured: false,
|
||||
gridPower: -6230,
|
||||
pvPower: 0,
|
||||
batteryPower: 0,
|
||||
batterySoC: 0,
|
||||
siteTitle: "Home",
|
||||
};
|
||||
202
assets/js/components/Energyflow/Energyflow.vue
Normal file
202
assets/js/components/Energyflow/Energyflow.vue
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<template>
|
||||
<div>
|
||||
<h4 class="d-none d-md-block my-4" v-if="siteTitle">
|
||||
{{ siteTitle }}
|
||||
</h4>
|
||||
<div
|
||||
class="row align-items-start align-items-md-center mt-4 energyflow"
|
||||
@click="toggleDetails"
|
||||
>
|
||||
<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
|
||||
"
|
||||
:showDetails="showDetails"
|
||||
:gridImport="gridImport"
|
||||
:selfConsumption="selfConsumption"
|
||||
:pvExport="pvExport"
|
||||
:batteryCharge="batteryCharge"
|
||||
:batteryDischarge="batteryDischarge"
|
||||
:pvProduction="pvProduction"
|
||||
:houseConsumption="houseConsumption"
|
||||
:batterySoC="batterySoC"
|
||||
/>
|
||||
<div
|
||||
class="col-12 col-sm-6 col-md-5 col-lg-3 col-xl-3 order-md-1 mt-2 mt-md-0"
|
||||
:class="`${showDetails ? 'd-block' : `d-none d-md-block`}`"
|
||||
>
|
||||
<div class="d-flex justify-content-between" data-test-pv-production>
|
||||
<span class="details-icon text-muted"><fa-icon icon="sun"></fa-icon></span>
|
||||
<span class="text-nowrap flex-grow-1">{{
|
||||
$t("main.energyflow.pvProduction")
|
||||
}}</span>
|
||||
<span class="text-end text-nowrap ps-1">{{ kw(pvProduction) }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between" data-test-house-consumption>
|
||||
<span class="details-icon text-muted"><fa-icon icon="home"></fa-icon></span>
|
||||
<span class="text-nowrap flex-grow-1">{{
|
||||
$t("main.energyflow.houseConsumption")
|
||||
}}</span>
|
||||
<span class="text-end text-nowrap ps-1">{{ kw(houseConsumption) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="batteryConfigured"
|
||||
class="d-flex justify-content-between"
|
||||
data-test-battery
|
||||
>
|
||||
<span class="details-icon text-muted">
|
||||
<BatteryIcon
|
||||
:soc="batterySoC"
|
||||
:charge="batteryCharge > 0"
|
||||
:discharge="batteryDischarge > 0"
|
||||
/>
|
||||
</span>
|
||||
<span class="text-nowrap flex-grow-1 text-truncate">
|
||||
<span v-if="batteryCharge">{{ $t("main.energyflow.batteryCharge") }}</span>
|
||||
<span v-else-if="batteryDischarge">{{
|
||||
$t("main.energyflow.batteryDischarge")
|
||||
}}</span>
|
||||
<span v-else>{{ $t("main.energyflow.battery") }}</span>
|
||||
</span>
|
||||
<span class="text-end text-nowrap ps-1">
|
||||
{{ batterySoC }}% /
|
||||
{{ kw(Math.abs(batteryPower)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
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
|
||||
"
|
||||
v-if="showDetails"
|
||||
>
|
||||
<div class="text-nowrap d-flex d-md-block" data-test-grid-import>
|
||||
<span class="color-grid details-icon"><fa-icon icon="square"></fa-icon></span>
|
||||
<span class="text-nowrap flex-grow-1">{{
|
||||
$t("main.energyflow.gridImport")
|
||||
}}</span>
|
||||
<span class="text-end text-nowrap d-md-none">{{ kw(gridImport) }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-nowrap d-flex d-md-block"
|
||||
data-test-self-consumption
|
||||
v-if="batteryConfigured || pvConfigured"
|
||||
>
|
||||
<span class="color-self details-icon"><fa-icon icon="square"></fa-icon></span>
|
||||
<span class="text-nowrap flex-grow-1">{{
|
||||
$t("main.energyflow.selfConsumption")
|
||||
}}</span>
|
||||
<span class="text-end text-nowrap d-md-none">{{ kw(selfConsumption) }}</span>
|
||||
</div>
|
||||
<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>
|
||||
<span class="text-nowrap flex-grow-1">{{
|
||||
$t("main.energyflow.pvExport")
|
||||
}}</span>
|
||||
<span class="text-end text-nowrap d-md-none">{{ kw(pvExport) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "../../icons";
|
||||
import formatter from "../../mixins/formatter";
|
||||
import Visualization from "./Visualization.vue";
|
||||
import BatteryIcon from "./BatteryIcon.vue";
|
||||
|
||||
export default {
|
||||
name: "Energyflow",
|
||||
components: { Visualization, BatteryIcon },
|
||||
props: {
|
||||
gridConfigured: Boolean,
|
||||
gridPower: { type: Number, default: 0 },
|
||||
pvConfigured: Boolean,
|
||||
pvPower: { type: Number, default: 0 },
|
||||
batteryConfigured: Boolean,
|
||||
batteryPower: { type: Number, default: 0 },
|
||||
batterySoC: { type: Number, default: 0 },
|
||||
siteTitle: { type: String },
|
||||
},
|
||||
data: function () {
|
||||
return { showDetails: false };
|
||||
},
|
||||
mixins: [formatter],
|
||||
computed: {
|
||||
gridImport: function () {
|
||||
return Math.max(0, this.gridPower);
|
||||
},
|
||||
pvProduction: function () {
|
||||
return this.pvConfigured ? Math.abs(this.pvPower) : this.pvExport;
|
||||
},
|
||||
pvConsumption: function () {
|
||||
return Math.min(
|
||||
this.pvProduction,
|
||||
this.pvProduction + this.gridPower - this.batteryCharge
|
||||
);
|
||||
},
|
||||
batteryDischarge: function () {
|
||||
return Math.max(0, this.batteryPower);
|
||||
},
|
||||
batteryCharge: function () {
|
||||
return Math.min(0, this.batteryPower) * -1;
|
||||
},
|
||||
houseConsumption: function () {
|
||||
return this.gridImport + this.pvConsumption + this.batteryDischarge;
|
||||
},
|
||||
selfConsumption: function () {
|
||||
return this.batteryDischarge + this.pvConsumption + this.batteryCharge;
|
||||
},
|
||||
pvExport: function () {
|
||||
return Math.min(0, this.gridPower) * -1;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
kw: function (watt) {
|
||||
return Math.max(0, watt / 1000).toFixed(1) + " kW";
|
||||
},
|
||||
toggleDetails() {
|
||||
this.showDetails = !this.showDetails;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.energyflow {
|
||||
cursor: pointer;
|
||||
}
|
||||
.color-grid {
|
||||
color: var(--evcc-grid);
|
||||
}
|
||||
.color-self {
|
||||
color: var(--evcc-self);
|
||||
}
|
||||
.color-export {
|
||||
color: var(--evcc-export);
|
||||
}
|
||||
.details-icon {
|
||||
text-align: center;
|
||||
width: 30px;
|
||||
margin-right: 0.25rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
277
assets/js/components/Energyflow/Visualization.vue
Normal file
277
assets/js/components/Energyflow/Visualization.vue
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="label-scale">
|
||||
<div class="d-flex justify-content-end">
|
||||
<div
|
||||
class="label-bar label-bar--down"
|
||||
:class="{
|
||||
'label-bar--hide-icon': hideLabelIcon(batteryDischarge),
|
||||
'label-bar--hidden': !selfConsumptionAdjusted,
|
||||
}"
|
||||
:style="{ width: widthTotal(batteryDischarge) }"
|
||||
>
|
||||
<div class="label-bar-scale">
|
||||
<div class="label-bar-icon">
|
||||
<BatteryIcon :soc="batterySoC" discharge />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="label-bar label-bar--down"
|
||||
:class="{
|
||||
'label-bar--hide-icon': hideLabelIcon(pvProduction),
|
||||
'label-bar--hidden': !selfConsumptionAdjusted && !pvExportAdjusted,
|
||||
}"
|
||||
:style="{ width: widthTotal(pvProduction) }"
|
||||
>
|
||||
<div class="label-bar-scale">
|
||||
<div class="label-bar-icon">
|
||||
<fa-icon icon="sun"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-progress" ref="site_progress">
|
||||
<div
|
||||
class="site-progress-bar grid-import"
|
||||
:style="{ width: widthTotal(gridImportAdjusted) }"
|
||||
>
|
||||
<span class="power" :class="{ 'd-none': hidePowerLabel(gridImport) }">
|
||||
{{ kw(gridImport) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="site-progress-bar self-consumption"
|
||||
:style="{ width: widthTotal(selfConsumptionAdjusted) }"
|
||||
>
|
||||
<span
|
||||
class="power"
|
||||
:class="{
|
||||
'd-none': hidePowerLabel(selfConsumption),
|
||||
}"
|
||||
>
|
||||
{{ kw(selfConsumption) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="site-progress-bar pv-export"
|
||||
:style="{ width: widthTotal(pvExportAdjusted) }"
|
||||
>
|
||||
<span class="power" :class="{ 'd-none': hidePowerLabel(pvExport) }">
|
||||
{{ kw(pvExport) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="site-progress-bar bg-light border no-wrap w-100" v-if="totalAdjusted <= 0">
|
||||
<span>{{ $t("main.energyflow.noEnergy") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label-scale">
|
||||
<div class="d-flex justify-content-start">
|
||||
<div
|
||||
class="label-bar label-bar--up"
|
||||
:class="{
|
||||
'label-bar--hide-icon': hideLabelIcon(houseConsumption),
|
||||
'label-bar--hidden': !gridImportAdjusted && !selfConsumptionAdjusted,
|
||||
}"
|
||||
:style="{ width: widthTotal(houseConsumption) }"
|
||||
>
|
||||
<div class="label-bar-scale">
|
||||
<div class="label-bar-icon">
|
||||
<fa-icon icon="home"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="label-bar label-bar--up"
|
||||
:class="{
|
||||
'label-bar--hide-icon': hideLabelIcon(batteryCharge),
|
||||
'label-bar--hidden': !selfConsumptionAdjusted,
|
||||
}"
|
||||
:style="{ width: widthTotal(batteryCharge) }"
|
||||
>
|
||||
<div class="label-bar-scale">
|
||||
<div class="label-bar-icon">
|
||||
<BatteryIcon :soc="batterySoC" charge />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "../../icons";
|
||||
import formatter from "../../mixins/formatter";
|
||||
import BatteryIcon from "./BatteryIcon.vue";
|
||||
|
||||
export default {
|
||||
name: "Visualization",
|
||||
components: { BatteryIcon },
|
||||
props: {
|
||||
showDetails: Boolean,
|
||||
gridImport: { type: Number, default: 0 },
|
||||
selfConsumption: { type: Number, default: 0 },
|
||||
pvExport: { type: Number, default: 0 },
|
||||
batteryCharge: { type: Number, default: 0 },
|
||||
batteryDischarge: { type: Number, default: 0 },
|
||||
pvProduction: { type: Number, default: 0 },
|
||||
houseConsumption: { type: Number, default: 0 },
|
||||
batterySoC: { type: Number, default: 0 },
|
||||
},
|
||||
data: function () {
|
||||
return { width: 0 };
|
||||
},
|
||||
mounted: function () {
|
||||
this.$nextTick(function () {
|
||||
window.addEventListener("resize", this.updateElementWidth);
|
||||
this.updateElementWidth();
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.updateElementWidth);
|
||||
},
|
||||
mixins: [formatter],
|
||||
computed: {
|
||||
totalRaw: function () {
|
||||
return this.gridImport + this.selfConsumption + this.pvExport;
|
||||
},
|
||||
gridImportAdjusted: function () {
|
||||
return this.applyThreshold(this.gridImport);
|
||||
},
|
||||
selfConsumptionAdjusted: function () {
|
||||
return this.applyThreshold(this.selfConsumption);
|
||||
},
|
||||
pvExportAdjusted: function () {
|
||||
return this.applyThreshold(this.pvExport);
|
||||
},
|
||||
totalAdjusted: function () {
|
||||
return this.gridImportAdjusted + this.selfConsumptionAdjusted + this.pvExportAdjusted;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showDetails: function () {
|
||||
this.$nextTick(() => this.updateElementWidth());
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
widthTotal: function (power) {
|
||||
if (this.totalAdjusted === 0) return "0%";
|
||||
return (100 / this.totalAdjusted) * power + "%";
|
||||
},
|
||||
kw: function (watt) {
|
||||
return Math.max(0, watt / 1000).toFixed(1) + " kW";
|
||||
},
|
||||
hidePowerLabel(power) {
|
||||
if (this.totalAdjusted === 0) return true;
|
||||
const minWidth = 75;
|
||||
const percent = (100 / this.totalAdjusted) * power;
|
||||
return (this.width / 100) * percent < minWidth;
|
||||
},
|
||||
hideLabelIcon(power) {
|
||||
if (this.totalAdjusted === 0) return true;
|
||||
const minWidth = 60;
|
||||
const percent = (100 / this.totalAdjusted) * power;
|
||||
return (this.width / 100) * percent < minWidth;
|
||||
},
|
||||
applyThreshold(power) {
|
||||
const percent = (100 / this.totalRaw) * power;
|
||||
return percent < 2 ? 0 : power;
|
||||
},
|
||||
updateElementWidth() {
|
||||
this.width = this.$refs.site_progress.getBoundingClientRect().width;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.site-progress {
|
||||
--height: 38px;
|
||||
height: var(--height);
|
||||
margin: 0.25rem 0;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.site-progress-bar {
|
||||
display: flex;
|
||||
transition-property: width;
|
||||
transition-duration: 500ms;
|
||||
transition-timing-function: linear;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.grid-import {
|
||||
background-color: var(--evcc-grid);
|
||||
color: var(--bs-white);
|
||||
}
|
||||
.self-consumption {
|
||||
background-color: var(--evcc-self);
|
||||
color: var(--bs-white);
|
||||
}
|
||||
.pv-export {
|
||||
background-color: var(--evcc-export);
|
||||
color: var(--bs-dark);
|
||||
}
|
||||
.power {
|
||||
display: block;
|
||||
margin: 0 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.label-bar {
|
||||
width: 0;
|
||||
margin: 0;
|
||||
height: 1.7rem;
|
||||
padding: 0.6rem 0;
|
||||
opacity: 1;
|
||||
transition-property: width, opacity;
|
||||
transition-duration: 500ms, 250ms;
|
||||
transition-timing-function: linear, ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.label-bar--down:first-child {
|
||||
margin-right: -1px;
|
||||
}
|
||||
.label-bar--up:last-child {
|
||||
margin-left: -1px;
|
||||
}
|
||||
.label-bar-scale {
|
||||
border: 1px solid var(--bs-gray);
|
||||
height: 0.5rem;
|
||||
background: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.label-bar--down .label-bar-scale {
|
||||
border-bottom: none;
|
||||
}
|
||||
.label-bar--up .label-bar-scale {
|
||||
border-top: none;
|
||||
}
|
||||
.label-bar-icon {
|
||||
background-color: white;
|
||||
color: var(--bs-gray);
|
||||
padding: 0 0.75rem;
|
||||
opacity: 1;
|
||||
transition: opacity 250ms ease-in;
|
||||
}
|
||||
.label-bar--down .label-bar-icon {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.label-bar--up .label-bar-icon {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.label-bar--hide-icon .label-bar-icon {
|
||||
opacity: 0;
|
||||
}
|
||||
.label-bar--hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
3
assets/js/components/Energyflow/index.js
Normal file
3
assets/js/components/Energyflow/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Energyflow from "./Energyflow.vue";
|
||||
|
||||
export default Energyflow;
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<div class="btn-group w-100" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{ active: mode == 'off' }"
|
||||
@click="setTargetMode('off')"
|
||||
>
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{ active: mode == 'now' }"
|
||||
@click="setTargetMode('now')"
|
||||
>
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{ active: mode == 'minpv' }"
|
||||
@click="setTargetMode('minpv')"
|
||||
>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{ active: mode == 'pv' }"
|
||||
@click="setTargetMode('pv')"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ export const Base = Template.bind({});
|
|||
Base.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
gridPower: 100,
|
||||
pvPower: 100,
|
||||
gridPower: 1340,
|
||||
pvPower: 6420,
|
||||
batteryConfigured: true,
|
||||
batteryPower: 100,
|
||||
batterySoC: 0,
|
||||
batteryPower: 800,
|
||||
batterySoC: 66,
|
||||
loadpoints: [],
|
||||
};
|
||||
|
||||
|
|
@ -30,11 +30,11 @@ export const Single = Template.bind({});
|
|||
Single.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
gridPower: 100,
|
||||
pvPower: 100,
|
||||
gridPower: -2300,
|
||||
pvPower: 7000,
|
||||
batteryConfigured: true,
|
||||
batteryPower: 100,
|
||||
batterySoC: 0,
|
||||
batteryPower: -2000,
|
||||
batterySoC: 24,
|
||||
loadpoints: [
|
||||
{
|
||||
title: "Ladepunkt 1",
|
||||
|
|
@ -59,10 +59,10 @@ export const Multi = Template.bind({});
|
|||
Multi.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
gridPower: -100,
|
||||
pvPower: 100,
|
||||
batteryConfigured: true,
|
||||
batteryPower: 100,
|
||||
gridPower: -1000,
|
||||
pvPower: 5500,
|
||||
batteryConfigured: false,
|
||||
batteryPower: 70,
|
||||
batterySoC: 60,
|
||||
loadpoints: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
<template>
|
||||
<div class="flex-grow-1 d-flex flex-column">
|
||||
<div class="row mt-4 pt-2">
|
||||
<div class="d-none d-md-flex col-12 col-md-3 col-lg-4 align-items-end">
|
||||
<p class="h1 text-truncate">{{ siteTitle || "Home" }}</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-9 col-lg-8 flex-grow-1">
|
||||
<SiteDetails v-bind="details"></SiteDetails>
|
||||
</div>
|
||||
</div>
|
||||
<Energyflow v-bind="energyflow" />
|
||||
<hr class="w-100 my-4" />
|
||||
<div class="flex-grow-1 d-flex justify-content-around flex-column">
|
||||
<template v-for="(loadpoint, id) in loadpoints">
|
||||
|
|
@ -24,7 +17,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import SiteDetails from "./SiteDetails";
|
||||
import Energyflow from "./Energyflow";
|
||||
import Loadpoint from "./Loadpoint";
|
||||
import formatter from "../mixins/formatter";
|
||||
import collector from "../mixins/collector";
|
||||
|
|
@ -32,7 +25,6 @@ import collector from "../mixins/collector";
|
|||
export default {
|
||||
name: "Site",
|
||||
props: {
|
||||
siteTitle: String,
|
||||
loadpoints: Array,
|
||||
|
||||
// details
|
||||
|
|
@ -45,12 +37,13 @@ export default {
|
|||
batterySoC: Number,
|
||||
gridCurrents: Array,
|
||||
prioritySoC: Number,
|
||||
siteTitle: String,
|
||||
},
|
||||
components: { SiteDetails, Loadpoint },
|
||||
components: { Loadpoint, Energyflow },
|
||||
mixins: [formatter, collector],
|
||||
computed: {
|
||||
details: function () {
|
||||
return this.collectProps(SiteDetails);
|
||||
energyflow: function () {
|
||||
return this.collectProps(Energyflow);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import SiteDetails from "./SiteDetails.vue";
|
||||
import i18n from "../i18n";
|
||||
|
||||
export default {
|
||||
title: "Main/SiteDetails",
|
||||
component: SiteDetails,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
i18n,
|
||||
props: Object.keys(argTypes),
|
||||
components: { SiteDetails },
|
||||
template: '<SiteDetails v-bind="$props"></SiteDetails>',
|
||||
});
|
||||
|
||||
export const GridAndPV = Template.bind({});
|
||||
GridAndPV.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
gridPower: 100,
|
||||
pvPower: 100,
|
||||
};
|
||||
|
||||
export const WithBattery = Template.bind({});
|
||||
WithBattery.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
gridPower: 100,
|
||||
pvPower: 100,
|
||||
batteryPower: 100,
|
||||
batterySoC: 0,
|
||||
};
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
<template>
|
||||
<div class="row justify-content-md-start overflow-hidden" :class="`row-cols-${numberOfPanels}`">
|
||||
<div class="px-3" :class="panelClass" v-if="gridConfigured">
|
||||
<div class="mb-2 value" v-if="gridPower > 0">
|
||||
{{ $t("main.siteDetails.import") }}
|
||||
<fa-icon icon="arrow-down" class="text-primary" />
|
||||
</div>
|
||||
<div class="mb-2 value" v-else>
|
||||
{{ $t("main.siteDetails.export") }}
|
||||
<fa-icon icon="arrow-up" class="text-primary"></fa-icon>
|
||||
</div>
|
||||
<h3 class="value">
|
||||
{{ fmt(gridPower) }}
|
||||
<small class="text-muted">{{ fmtUnit(gridPower) }}W</small>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="px-3" :class="panelClass" v-if="pvConfigured">
|
||||
<div class="mb-2 value">
|
||||
{{ $t("main.siteDetails.production") }}
|
||||
<fa-icon
|
||||
icon="sun"
|
||||
:class="{
|
||||
'text-primary': pvPower > 0,
|
||||
'text-muted': pvPower <= 0,
|
||||
}"
|
||||
></fa-icon>
|
||||
</div>
|
||||
<h3 class="value">
|
||||
{{ fmt(pvPower) }}
|
||||
<small class="text-muted">{{ fmtUnit(pvPower) }}W</small>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="px-3" :class="panelClass" v-if="batteryConfigured">
|
||||
<div class="mb-2 value">
|
||||
<div class="d-block d-sm-none">
|
||||
{{ $t("main.siteDetails.batteryShort") }}
|
||||
<span class="text-muted"> / {{ batterySoC }} %</span>
|
||||
</div>
|
||||
<div class="d-none d-sm-block">
|
||||
{{ $t("main.siteDetails.batteryLong") }}
|
||||
<span class="text-muted"> / {{ batterySoC }}% </span>
|
||||
<fa-icon class="text-primary" :icon="batteryIcon"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="value">
|
||||
{{ fmt(batteryPower) }}
|
||||
<small class="text-muted">{{ fmtUnit(batteryPower) }}W</small>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "../icons";
|
||||
import formatter from "../mixins/formatter";
|
||||
|
||||
const limit = 20;
|
||||
const icons = [
|
||||
"battery-empty",
|
||||
"battery-quarter",
|
||||
"battery-half",
|
||||
"battery-three-quarters",
|
||||
"battery-full",
|
||||
];
|
||||
|
||||
export default {
|
||||
name: "SiteDetails",
|
||||
props: {
|
||||
gridConfigured: Boolean,
|
||||
gridPower: Number,
|
||||
pvConfigured: Boolean,
|
||||
pvPower: Number,
|
||||
batteryConfigured: Boolean,
|
||||
batteryPower: Number,
|
||||
batterySoC: Number,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
iconIdx: 0,
|
||||
};
|
||||
},
|
||||
mixins: [formatter],
|
||||
computed: {
|
||||
numberOfPanels: function () {
|
||||
let count = 0;
|
||||
if (this.gridConfigured) count++;
|
||||
if (this.pvConfigured) count++;
|
||||
if (this.batteryConfigured) count++;
|
||||
return count;
|
||||
},
|
||||
panelClass: function () {
|
||||
if (this.numberOfPanels == 3) {
|
||||
return "col-4 col-md-4 col-lg-3";
|
||||
}
|
||||
if (this.numberOfPanels == 2) {
|
||||
return "col-6 col-md-4 col-lg-6";
|
||||
}
|
||||
return "col-12";
|
||||
},
|
||||
batteryIcon: function () {
|
||||
if (Math.abs(this.batteryPower) < limit) {
|
||||
if (this.batterySoC < 30) return icons[0];
|
||||
if (this.batterySoC < 50) return icons[1];
|
||||
if (this.batterySoC < 70) return icons[2];
|
||||
if (this.batterySoC < 90) return icons[3];
|
||||
return icons[4];
|
||||
}
|
||||
return icons[this.iconIdx];
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
window.setInterval(() => {
|
||||
if (this.batteryPower > limit) {
|
||||
if (--this.iconIdx < 0) {
|
||||
this.iconIdx = icons.length - 1;
|
||||
}
|
||||
} else if (this.batteryPower < limit) {
|
||||
if (++this.iconIdx >= icons.length) {
|
||||
this.iconIdx = 0;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -29,12 +29,16 @@ export default {
|
|||
dismissAll: "Meldungen entfernen",
|
||||
},
|
||||
main: {
|
||||
siteDetails: {
|
||||
import: "Bezug",
|
||||
export: "Einspeisung",
|
||||
production: "Erzeugung",
|
||||
batteryShort: "Akku",
|
||||
batteryLong: "Batterie",
|
||||
energyflow: {
|
||||
noEnergy: "Kein Energiefluss",
|
||||
houseConsumption: "Verbrauch",
|
||||
pvProduction: "Erzeugung",
|
||||
battery: "Batterie",
|
||||
batteryCharge: "Batterie laden",
|
||||
batteryDischarge: "Batterie entladen",
|
||||
gridImport: "Netzbezug",
|
||||
selfConsumption: "Eigenverbrauch",
|
||||
pvExport: "Einspeisung",
|
||||
},
|
||||
mode: {
|
||||
title: "Modus",
|
||||
|
|
|
|||
|
|
@ -29,12 +29,16 @@ export default {
|
|||
dismissAll: "Dismiss all",
|
||||
},
|
||||
main: {
|
||||
siteDetails: {
|
||||
import: "Import",
|
||||
export: "Export",
|
||||
production: "Production",
|
||||
batteryShort: "Batt.",
|
||||
batteryLong: "Battery",
|
||||
energyflow: {
|
||||
noEnergy: "No Energyflow",
|
||||
houseConsumption: "Consumption",
|
||||
pvProduction: "Production",
|
||||
battery: "Battery",
|
||||
batteryCharge: "Battery charge",
|
||||
batteryDischarge: "Battery discharge",
|
||||
gridImport: "Grid import",
|
||||
selfConsumption: "Self consumption",
|
||||
pvExport: "Grid feed-in",
|
||||
},
|
||||
mode: {
|
||||
title: "Mode",
|
||||
|
|
|
|||
|
|
@ -29,12 +29,16 @@ export default {
|
|||
dismissAll: "Rimuovi tutte",
|
||||
},
|
||||
main: {
|
||||
siteDetails: {
|
||||
import: "Import",
|
||||
export: "Export",
|
||||
production: "Produzione",
|
||||
batteryShort: "Batt.",
|
||||
batteryLong: "Batteria",
|
||||
energyflow: {
|
||||
noEnergy: "No Energyflow",
|
||||
houseConsumption: "Consumption",
|
||||
pvProduction: "Produzione",
|
||||
battery: "Batteria",
|
||||
batteryCharge: "Battery charge",
|
||||
batteryDischarge: "Battery discharge",
|
||||
gridImport: "Grid import",
|
||||
selfConsumption: "Self consumption",
|
||||
pvExport: "Grid feed-in",
|
||||
},
|
||||
mode: {
|
||||
title: "Modalità",
|
||||
|
|
|
|||
|
|
@ -2,18 +2,15 @@ import Vue from "vue";
|
|||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
import { faArrowDown } from "@fortawesome/free-solid-svg-icons/faArrowDown";
|
||||
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
|
||||
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 { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
|
||||
import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
|
||||
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 { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons/faExclamationTriangle";
|
||||
import { faLeaf } from "@fortawesome/free-solid-svg-icons/faLeaf";
|
||||
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
|
||||
import { faTemperatureHigh } from "@fortawesome/free-solid-svg-icons/faTemperatureHigh";
|
||||
import { faTemperatureLow } from "@fortawesome/free-solid-svg-icons/faTemperatureLow";
|
||||
|
|
@ -22,30 +19,37 @@ 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 { 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(
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faAngleDown,
|
||||
faAngleUp,
|
||||
faBatteryEmpty,
|
||||
faBatteryFull,
|
||||
faBatteryHalf,
|
||||
faBatteryQuarter,
|
||||
faBatteryThreeQuarters,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faBox,
|
||||
faCar,
|
||||
faAngleDoubleLeft,
|
||||
faAngleDoubleRight,
|
||||
faClock,
|
||||
faExclamationCircle,
|
||||
faExclamationTriangle,
|
||||
faLeaf,
|
||||
faGift,
|
||||
faHome,
|
||||
farHeart,
|
||||
fasHeart,
|
||||
faSquare,
|
||||
faSun,
|
||||
faTemperatureHigh,
|
||||
faTemperatureLow,
|
||||
faThermometerHalf,
|
||||
farHeart,
|
||||
fasHeart,
|
||||
faGift,
|
||||
faBox,
|
||||
faExclamationCircle
|
||||
faThermometerHalf
|
||||
);
|
||||
|
||||
Vue.component("fa-icon", FontAwesomeIcon);
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
<div class="flex-grow-1 d-flex flex-column justify-content-between">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="https://github.com/andig/evcc">
|
||||
<Logo class="logo"></Logo>
|
||||
<a class="navbar-brand" href="/" @click.prevent="reload">
|
||||
<Logo class="logo me-3"></Logo>
|
||||
</a>
|
||||
<div class="d-flex">
|
||||
<div class="d-flex">
|
||||
|
|
@ -94,6 +94,14 @@ export default {
|
|||
}
|
||||
};
|
||||
},
|
||||
reload() {
|
||||
window.location.reload();
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.title ? `evcc | ${this.title}` : "evcc",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
version: function () {
|
||||
|
|
@ -106,6 +114,9 @@ export default {
|
|||
uploadProgress: this.store.state.uploadProgress,
|
||||
};
|
||||
},
|
||||
title: function () {
|
||||
return this.store.state.siteTitle;
|
||||
},
|
||||
sponsor: function () {
|
||||
return this.store.state.sponsor;
|
||||
},
|
||||
|
|
@ -127,4 +138,8 @@ export default {
|
|||
.app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.title {
|
||||
position: relative;
|
||||
top: 0.1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
1
dist/css/index.d2c9ca1b.css
vendored
Normal file
1
dist/css/index.d2c9ca1b.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/css/index.f1264e7c.css
vendored
1
dist/css/index.f1264e7c.css
vendored
|
|
@ -1 +0,0 @@
|
|||
.progress-bar-animated{animation-direction:reverse}.btn.caption{opacity:1}.btn.first{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}.value,input[type=radio],label.btn{white-space:nowrap!important}.text-muted a,.text-muted a:hover{color:#6c757d!important;text-decoration:none}.bg-muted{opacity:.25}.icon[data-v-99c2e68a]{color:#0fdd42}.icon[data-v-1eecc7b7]{color:#0fdd42;display:inline-block}.sponsor-button[data-v-1eecc7b7]{touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.support-button .solid[data-v-1eecc7b7]{display:none}.support-button:hover .solid[data-v-1eecc7b7]{display:inline-block}.support-button:hover .outline[data-v-1eecc7b7]{display:none}.logo[data-v-1d8baa41]{width:85px}.app[data-v-1d8baa41]{min-height:100vh}.btn[data-v-5d01faa2]{flex-basis:0;white-space:nowrap}.vehicle-soc[data-v-3405be3f]{--height:38px;--thumb-overlap:3px;--thumb-width:3px;--thumb-horizontal-padding:15px;--label-height:26px;position:relative;height:var(--height)}.progress[data-v-3405be3f]{height:100%;font-size:1rem}.progress-bar.bg-muted[data-v-3405be3f]{color:var(--white)}.bg-light[data-v-3405be3f]{color:var(--bs-gray-dark)}.target-label[data-v-3405be3f]{width:3em;margin-left:-1.5em;height:var(--label-height);position:absolute;top:calc(var(--label-height)*-1 + var(--thumb-overlap)*-1);text-align:center;color:var(--bs-gray-dark);font-size:1rem;opacity:1;transition:opacity .2s ease 1s}.target-slider[data-v-3405be3f]{-webkit-appearance:none;position:absolute;top:calc(var(--thumb-overlap)*-1);left:calc(var(--thumb-horizontal-padding)*-1);height:calc(100% + var(--thumb-overlap)*2);width:calc(100% + var(--thumb-horizontal-padding)*2);background:transparent}.target-slider[data-v-3405be3f]:focus{outline:none}.target-slider[data-v-3405be3f]::-webkit-slider-runnable-track{position:relative;background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-3405be3f]::-moz-range-track{background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-3405be3f]::-webkit-slider-thumb{-webkit-appearance:none;position:relative;top:calc(var(--label-height)*-1);height:100%;width:var(--thumb-width);padding:var(--label-height) var(--thumb-horizontal-padding) 0;box-sizing:content-box;background-clip:content-box;background-color:var(--bs-gray-dark);cursor:-webkit-grab;cursor:grab;border:none;opacity:1;-webkit-transition:opacity .2s ease 1s;transition:opacity .2s ease 1s}.target-slider[data-v-3405be3f]::-moz-range-thumb{position:relative;top:calc(var(--label-height)*-1);height:100%;width:var(--thumb-width);padding:0 var(--thumb-horizontal-padding) 0;box-sizing:content-box;background-clip:content-box;background-color:var(--bs-gray-dark);cursor:grab;border:none;opacity:1;-moz-transition:opacity .2s ease 1s;transition:opacity .2s ease 1s}.target--max .target-label[data-v-3405be3f],.target--max .target-slider[data-v-3405be3f]::-webkit-slider-thumb{opacity:0}.target--max .target-label[data-v-3405be3f],.target--max .target-slider[data-v-3405be3f]::-moz-range-thumb{opacity:0}.target:hover .target-label[data-v-3405be3f],.target:hover .target-slider[data-v-3405be3f]::-webkit-slider-thumb{opacity:1;transition-delay:0s}.target:hover .target-label[data-v-3405be3f],.target:hover .target-slider[data-v-3405be3f]::-moz-range-thumb{opacity:1;transition-delay:0s}
|
||||
4
dist/index.html
vendored
4
dist/index.html
vendored
|
|
@ -1,4 +1,4 @@
|
|||
<!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.882a611f.css" rel="preload" as="style"><link href="css/index.f1264e7c.css" rel="preload" as="style"><link href="js/chunk-vendors.8d9f525d.js" rel="preload" as="script"><link href="js/index.3e99d31f.js" rel="preload" as="script"><link href="css/chunk-vendors.882a611f.css" rel="stylesheet"><link href="css/index.f1264e7c.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="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.882a611f.css" rel="preload" as="style"><link href="css/index.d2c9ca1b.css" rel="preload" as="style"><link href="js/chunk-vendors.1a7ce110.js" rel="preload" as="script"><link href="js/index.6e9352a0.js" rel="preload" as="script"><link href="css/chunk-vendors.882a611f.css" rel="stylesheet"><link href="css/index.d2c9ca1b.css" rel="stylesheet"></head><body><script>window.evcc = {
|
||||
version: "[[.Version]]",
|
||||
configured: "[[.Configured]]",
|
||||
};</script><div id="app"></div><script src="js/chunk-vendors.8d9f525d.js"></script><script src="js/index.3e99d31f.js"></script></body></html>
|
||||
};</script><div id="app"></div><script src="js/chunk-vendors.1a7ce110.js"></script><script src="js/index.6e9352a0.js"></script></body></html>
|
||||
File diff suppressed because one or more lines are too long
1
dist/js/chunk-vendors.1a7ce110.js.map
vendored
Normal file
1
dist/js/chunk-vendors.1a7ce110.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/js/chunk-vendors.8d9f525d.js.map
vendored
1
dist/js/chunk-vendors.8d9f525d.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/js/index.3e99d31f.js
vendored
2
dist/js/index.3e99d31f.js
vendored
File diff suppressed because one or more lines are too long
1
dist/js/index.3e99d31f.js.map
vendored
1
dist/js/index.3e99d31f.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/js/index.6e9352a0.js
vendored
Normal file
2
dist/js/index.6e9352a0.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/js/index.6e9352a0.js.map
vendored
Normal file
1
dist/js/index.6e9352a0.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
0
docker/bin/docker-btmgmt
Executable file → Normal file
0
docker/bin/docker-btmgmt
Executable file → Normal file
4
jest.config.js
Normal file
4
jest.config.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
preset: "@vue/cli-plugin-unit-jest/presets/no-babel",
|
||||
testMatch: ["**/*.spec.js"],
|
||||
};
|
||||
5948
package-lock.json
generated
5948
package-lock.json
generated
File diff suppressed because it is too large
Load diff
77
package.json
77
package.json
|
|
@ -1,45 +1,16 @@
|
|||
{
|
||||
"name": "evcc",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service build --watch --mode=development",
|
||||
"build": "vue-cli-service build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"lint": "prettier assets --write && eslint assets/**/*.js assets/**/*.vue --fix",
|
||||
"storybook": "start-storybook -p 6006 -s assets,node_modules/bootstrap/dist",
|
||||
"build-storybook": "build-storybook"
|
||||
},
|
||||
"author": "naltatis & andig",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^6.2.9",
|
||||
"@storybook/addon-essentials": "^6.2.9",
|
||||
"@storybook/addon-links": "^6.2.9",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/vue": "^6.2.9",
|
||||
"@vue/cli-service": "^4.5.13",
|
||||
"@vue/component-compiler-utils": "^3.2.0",
|
||||
"@vue/eslint-config-standard": "^6.0.0",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^7.9.0",
|
||||
"prettier": "^2.3.0",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
"scripts": {
|
||||
"build": "vue-cli-service build",
|
||||
"test": "vue-cli-service test:unit",
|
||||
"lint": "prettier assets --write && eslint assets/**/*.js assets/**/*.vue --fix",
|
||||
"build-storybook": "build-storybook",
|
||||
"dev": "vue-cli-service build --watch --mode=development",
|
||||
"storybook": "start-storybook -p 6006 -s assets,node_modules/bootstrap/dist"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=6.0.0",
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"alias": {
|
||||
"vue": "./node_modules/vue/dist/vue.js"
|
||||
},
|
||||
"repository": "github:andig/evcc",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.3",
|
||||
|
|
@ -51,8 +22,40 @@
|
|||
"vue": "^2.6.12",
|
||||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-i18n": "^8.24.4",
|
||||
"vue-meta": "^2.4.0",
|
||||
"vue-router": "^3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^6.2.9",
|
||||
"@storybook/addon-essentials": "^6.2.9",
|
||||
"@storybook/addon-links": "^6.2.9",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/vue": "^6.2.9",
|
||||
"@vue/cli-plugin-unit-jest": "^4.5.13",
|
||||
"@vue/cli-service": "^4.5.13",
|
||||
"@vue/component-compiler-utils": "^3.2.0",
|
||||
"@vue/eslint-config-standard": "^6.0.0",
|
||||
"@vue/test-utils": "^1.0.3",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^7.9.0",
|
||||
"prettier": "^2.3.0",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
},
|
||||
"alias": {
|
||||
"vue": "./node_modules/vue/dist/vue.js"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=6.0.0",
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": "github:andig/evcc",
|
||||
"resolutions": {
|
||||
"node-forge": "0.10.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue