Cleanup ui layout (#835)
This commit is contained in:
parent
5681d1996b
commit
fb4dde7ce6
27 changed files with 564 additions and 543 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export default {
|
|||
component: Loadpoint,
|
||||
argTypes: {
|
||||
mode: { control: { type: "inline-radio", options: ["off", "now", "minpv", "pv"] } },
|
||||
remoteDisabled: { control: { type: "radio", options: ["", "soft", "hard"] } },
|
||||
climater: { control: { type: "inline-radio", options: ["on", "heating", "cooling"] } },
|
||||
},
|
||||
};
|
||||
|
|
@ -19,11 +20,29 @@ export const Base = Template.bind({});
|
|||
Base.args = {
|
||||
id: 0,
|
||||
pvConfigured: true,
|
||||
chargePower: 2800,
|
||||
chargedEnergy: 11e3,
|
||||
chargeDuration: 95 * 60,
|
||||
hasVehicle: true,
|
||||
socTitle: "Mein Auto",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
charging: true,
|
||||
socCharge: 66,
|
||||
targetSoC: 90,
|
||||
};
|
||||
|
||||
export const WithLevels = Template.bind({});
|
||||
WithLevels.args = {
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
id: 0,
|
||||
pvConfigured: true,
|
||||
socLevels: [20, 50, 80, 100],
|
||||
remoteDisabled: "soft",
|
||||
remoteDisabledSource: "Sunny Home Manager",
|
||||
hasVehicle: true,
|
||||
socTitle: "Mein Auto",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
charging: false,
|
||||
socCharge: 66,
|
||||
targetSoC: 100,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,143 +1,32 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="row" v-if="multi">
|
||||
<div class="col-12 col-md-4 d-md-flex mt-3 mt-md-5 align-items-end">
|
||||
<span class="h1 align-bottom">{{ title || "Ladepunkt" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-8 d-none d-md-block mt-3 mt-md-5">
|
||||
<LoadpointDetails v-bind="details"> </LoadpointDetails>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-md-none">
|
||||
<div class="row mt-3 pb-3 bg-light">
|
||||
<div class="col-12 mt-3">
|
||||
<Mode
|
||||
class="w-100"
|
||||
:mode="mode"
|
||||
:pvConfigured="pvConfigured"
|
||||
v-on:updated="setTargetMode"
|
||||
></Mode>
|
||||
</div>
|
||||
<div class="col-12 mt-3" v-if="hasTargetSoC">
|
||||
<Soc
|
||||
class="w-100"
|
||||
:soc="targetSoC"
|
||||
:levels="socLevels"
|
||||
v-on:updated="setTargetSoC"
|
||||
></Soc>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="h3 mb-4 d-sm-block" :class="{ 'd-none': single }">{{ title || "Ladepunkt" }}</p>
|
||||
<div class="alert alert-warning mt-4 mb-2" role="alert" v-if="remoteDisabled == 'soft'">
|
||||
{{ remoteDisabledSource }}: Adaptives PV-Laden deaktiviert
|
||||
</div>
|
||||
|
||||
<div class="row d-none d-md-flex mt-5 py-3 pb-4 text-center bg-light" v-if="!multi">
|
||||
<div class="mt-3" :class="{ 'col-md-6': hasTargetSoC, 'col-md-12': !hasTargetSoC }">
|
||||
<Mode
|
||||
:mode="mode"
|
||||
:pvConfigured="pvConfigured"
|
||||
:caption="true"
|
||||
v-on:updated="setTargetMode"
|
||||
></Mode>
|
||||
</div>
|
||||
<div class="col-md-6 mt-3" v-if="hasTargetSoC">
|
||||
<Soc
|
||||
:soc="targetSoC"
|
||||
:levels="socLevels"
|
||||
:caption="true"
|
||||
v-on:updated="setTargetSoC"
|
||||
></Soc>
|
||||
<!-- <div class="btn-group btn-group-toggle bg-white shadow-none">
|
||||
<label class="btn btn-outline-primary">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="disabled"
|
||||
v-on:click="alert('not implemented - use api')"
|
||||
/>
|
||||
<fa-icon
|
||||
icon="clock"
|
||||
v-bind:class="{ fas: timerActive, far: !timerActive }"
|
||||
></fa-icon>
|
||||
</label>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="alert alert-danger mt-4 mb-2" role="alert" v-if="remoteDisabled == 'hard'">
|
||||
{{ remoteDisabledSource }}: Deaktiviert
|
||||
</div>
|
||||
|
||||
<div class="row d-md-none mt-2 pb-3 bg-light" v-if="!multi">
|
||||
<div class="col-12 mt-3">
|
||||
<Mode
|
||||
class="w-100"
|
||||
:mode="mode"
|
||||
:pvConfigured="pvConfigured"
|
||||
v-on:updated="setTargetMode"
|
||||
></Mode>
|
||||
</div>
|
||||
<div class="col-12 mt-3" v-if="hasTargetSoC">
|
||||
<Soc
|
||||
class="w-100"
|
||||
:soc="targetSoC"
|
||||
:levels="socLevels"
|
||||
v-on:updated="setTargetSoC"
|
||||
></Soc>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="!multi">
|
||||
<div class="col-12 col-md-4 d-none d-md-flex mt-3 mt-md-5">
|
||||
<span class="h1">{{ title || "Ladepunkt" }}</span>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 d-flex d-md-flex mt-3 mt-md-5 pt-3" v-if="remoteDisabled">
|
||||
<h5 class="w-100">
|
||||
<span class="badge badge-warning w-100" v-if="remoteDisabled == 'soft'">
|
||||
{{ remoteDisabledSource }}: Adaptives PV-Laden deaktiviert
|
||||
</span>
|
||||
<span class="badge badge-danger w-100" v-if="remoteDisabled == 'hard'">
|
||||
{{ remoteDisabledSource }}: Deaktiviert
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row border-bottom d-none d-md-block"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 mt-3 mb-3 mb-md-0">
|
||||
<Vehicle v-bind="vehicle"></Vehicle>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 d-none d-md-block mt-3" v-if="multi">
|
||||
<div class="mb-2 pb-1">Modus</div>
|
||||
<Mode
|
||||
class="btn-group-sm"
|
||||
:mode="mode"
|
||||
:pvConfigured="pvConfigured"
|
||||
v-on:updated="setTargetMode"
|
||||
></Mode>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 d-none d-md-block mt-3" v-if="multi && hasTargetSoC">
|
||||
<div class="mb-2 pb-1">Ladeziel</div>
|
||||
<Soc
|
||||
class="btn-group-sm"
|
||||
:soc="targetSoC"
|
||||
:levels="socLevels"
|
||||
v-on:updated="setTargetSoC"
|
||||
></Soc>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 d-none d-md-block" v-if="!multi">
|
||||
<LoadpointDetails v-bind="details"></LoadpointDetails>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-md-none">
|
||||
<LoadpointDetails v-bind="details"></LoadpointDetails>
|
||||
</div>
|
||||
<Mode
|
||||
class="col-12 col-md-6 col-lg-4 mb-4"
|
||||
:mode="mode"
|
||||
:pvConfigured="pvConfigured"
|
||||
v-on:updated="setTargetMode"
|
||||
/>
|
||||
<Vehicle
|
||||
class="col-12 col-md-6 col-lg-8 mb-4"
|
||||
v-bind="vehicle"
|
||||
@target-soc-updated="setTargetSoC"
|
||||
/>
|
||||
</div>
|
||||
<LoadpointDetails v-bind="details" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Soc from "./Soc";
|
||||
import Mode from "./Mode";
|
||||
import Vehicle from "./Vehicle";
|
||||
import LoadpointDetails from "./LoadpointDetails";
|
||||
|
|
@ -148,14 +37,13 @@ export default {
|
|||
name: "Loadpoint",
|
||||
props: {
|
||||
id: Number,
|
||||
multi: Boolean,
|
||||
pvConfigured: Boolean,
|
||||
single: Boolean,
|
||||
|
||||
// main
|
||||
title: String,
|
||||
mode: String,
|
||||
targetSoC: Number,
|
||||
socLevels: Array,
|
||||
remoteDisabled: Boolean,
|
||||
remoteDisabledSource: String,
|
||||
chargeDuration: Number,
|
||||
|
|
@ -180,8 +68,20 @@ export default {
|
|||
climater: String,
|
||||
range: Number,
|
||||
chargeEstimate: Number,
|
||||
|
||||
// other information
|
||||
phases: Number,
|
||||
minCurrent: Number,
|
||||
maxCurrent: Number,
|
||||
activePhases: Number,
|
||||
chargeCurrent: Number,
|
||||
socCapacity: Number,
|
||||
connectedDuration: Number,
|
||||
chargeCurrents: Object,
|
||||
chargeConfigured: Boolean,
|
||||
chargeRemainingEnergy: Number,
|
||||
},
|
||||
components: { LoadpointDetails, Soc, Mode, Vehicle },
|
||||
components: { LoadpointDetails, Mode, Vehicle },
|
||||
mixins: [formatter, collector],
|
||||
data: function () {
|
||||
return {
|
||||
|
|
@ -196,9 +96,6 @@ export default {
|
|||
vehicle: function () {
|
||||
return this.collectProps(Vehicle);
|
||||
},
|
||||
hasTargetSoC: function () {
|
||||
return this.socLevels != null && this.socLevels.length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
chargeDuration: function () {
|
||||
|
|
|
|||
|
|
@ -1,59 +1,62 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-3 mt-3">
|
||||
<div class="mb-2 value">
|
||||
Leistung
|
||||
<fa-icon
|
||||
class="text-primary ml-1"
|
||||
icon="temperature-low"
|
||||
v-if="climater == 'heating'"
|
||||
></fa-icon>
|
||||
<fa-icon
|
||||
class="text-primary ml-1"
|
||||
icon="temperature-high"
|
||||
v-if="climater == 'cooling'"
|
||||
></fa-icon>
|
||||
<fa-icon
|
||||
class="text-primary ml-1"
|
||||
icon="thermometer-half"
|
||||
v-if="climater == 'on'"
|
||||
></fa-icon>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-6 col-sm-3 col-lg-2 mt-3 offset-lg-4">
|
||||
<div class="mb-2 value">
|
||||
Leistung
|
||||
<fa-icon
|
||||
class="text-primary ml-1"
|
||||
icon="temperature-low"
|
||||
v-if="climater == 'heating'"
|
||||
></fa-icon>
|
||||
<fa-icon
|
||||
class="text-primary ml-1"
|
||||
icon="temperature-high"
|
||||
v-if="climater == 'cooling'"
|
||||
></fa-icon>
|
||||
<fa-icon
|
||||
class="text-primary ml-1"
|
||||
icon="thermometer-half"
|
||||
v-if="climater == 'on'"
|
||||
></fa-icon>
|
||||
</div>
|
||||
<h3 class="value">
|
||||
{{ fmt(chargePower) }}
|
||||
<small class="text-muted">{{ fmtUnit(chargePower) }}W</small>
|
||||
</h3>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
{{ fmt(chargePower) }}
|
||||
<small class="text-muted">{{ fmtUnit(chargePower) }}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 mt-3">
|
||||
<div class="mb-2 value">Geladen</div>
|
||||
<h2 class="value">
|
||||
{{ fmt(chargedEnergy) }}
|
||||
<small class="text-muted">{{ fmtUnit(chargedEnergy) }}Wh</small>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3 mt-3" v-if="range >= 0">
|
||||
<div class="mb-2 value">Reichweite</div>
|
||||
<h2 class="value">
|
||||
{{ Math.round(range) }}
|
||||
<small class="text-muted">km</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3 col-lg-2 mt-3">
|
||||
<div class="mb-2 value">Geladen</div>
|
||||
<h3 class="value">
|
||||
{{ fmt(chargedEnergy) }}
|
||||
<small class="text-muted">{{ fmtUnit(chargedEnergy) }}Wh</small>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3 mt-3" v-else>
|
||||
<div class="mb-2 value">Dauer</div>
|
||||
<h2 class="value">
|
||||
{{ fmtShortDuration(chargeDuration) }}
|
||||
<small class="text-muted">{{ fmtShortDurationUnit(chargeDuration) }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3 col-lg-2 mt-3" v-if="range >= 0">
|
||||
<div class="mb-2 value">Reichweite</div>
|
||||
<h3 class="value">
|
||||
{{ Math.round(range) }}
|
||||
<small class="text-muted">km</small>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3 mt-3" v-if="hasVehicle">
|
||||
<div class="mb-2 value">Restzeit</div>
|
||||
<h2 class="value">
|
||||
{{ fmtShortDuration(chargeEstimate) }}
|
||||
<small class="text-muted">{{ fmtShortDurationUnit(chargeEstimate) }}</small>
|
||||
</h2>
|
||||
<div class="col-6 col-sm-3 col-lg-2 mt-3" v-else>
|
||||
<div class="mb-2 value">Dauer</div>
|
||||
<h3 class="value">
|
||||
{{ fmtShortDuration(chargeDuration) }}
|
||||
<small class="text-muted">{{ fmtShortDurationUnit(chargeDuration) }}</small>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-sm-3 col-lg-2 mt-3" v-if="hasVehicle">
|
||||
<div class="mb-2 value">Restzeit</div>
|
||||
<h3 class="value">
|
||||
{{ fmtShortDuration(chargeEstimate) }}
|
||||
<small class="text-muted">{{ fmtShortDurationUnit(chargeEstimate) }}</small>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,35 +1,32 @@
|
|||
<template>
|
||||
<div class="btn-group btn-group-toggle bg-white shadow-none">
|
||||
<label
|
||||
class="btn btn-outline-primary value disabled caption font-weight-bold"
|
||||
v-if="caption"
|
||||
>
|
||||
Modus
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" :class="{ active: mode == 'off', first: !caption }">
|
||||
<input type="radio" value="off" v-on:click="setTargetMode('off')" />Stop
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" :class="{ active: mode == 'now' }">
|
||||
<input type="radio" value="now" v-on:click="setTargetMode('now')" />Sofort
|
||||
</label>
|
||||
<label
|
||||
class="btn btn-outline-primary"
|
||||
:class="{ active: mode == 'minpv' }"
|
||||
v-if="pvConfigured"
|
||||
>
|
||||
<input type="radio" value="minpv" v-on:click="setTargetMode('minpv')" />
|
||||
<span class="d-inline d-lg-none">Min</span>
|
||||
<span class="d-none d-lg-inline">Min + PV</span>
|
||||
</label>
|
||||
<label
|
||||
class="btn btn-outline-primary"
|
||||
:class="{ active: mode == 'pv' }"
|
||||
v-if="pvConfigured"
|
||||
>
|
||||
<input type="radio" value="pv" v-on:click="setTargetMode('pv')" />
|
||||
<span class="d-inline d-md-none">PV</span>
|
||||
<span class="d-none d-md-inline">Nur PV</span>
|
||||
</label>
|
||||
<div>
|
||||
<div class="mb-3">Modus</div>
|
||||
<div class="btn-group btn-group-toggle bg-white shadow-none w-100">
|
||||
<label class="btn btn-outline-primary first" :class="{ active: mode == 'off' }">
|
||||
<input type="radio" value="off" @click="setTargetMode('off')" />Stop
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" :class="{ active: mode == 'now' }">
|
||||
<input type="radio" value="now" @click="setTargetMode('now')" />Sofort
|
||||
</label>
|
||||
<label
|
||||
class="btn btn-outline-primary"
|
||||
:class="{ active: mode == 'minpv' }"
|
||||
v-if="pvConfigured"
|
||||
>
|
||||
<input type="radio" value="minpv" @click="setTargetMode('minpv')" />
|
||||
<span class="d-inline d-sm-none">Min</span>
|
||||
<span class="d-none d-sm-inline">Min + PV</span>
|
||||
</label>
|
||||
<label
|
||||
class="btn btn-outline-primary"
|
||||
:class="{ active: mode == 'pv' }"
|
||||
v-if="pvConfigured"
|
||||
>
|
||||
<input type="radio" value="pv" @click="setTargetMode('pv')" />
|
||||
<span class="d-inline d-sm-none">PV</span>
|
||||
<span class="d-none d-sm-inline">Nur PV</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -39,7 +36,6 @@ export default {
|
|||
props: {
|
||||
mode: String,
|
||||
pvConfigured: Boolean,
|
||||
caption: Boolean,
|
||||
},
|
||||
methods: {
|
||||
setTargetMode: function (mode) {
|
||||
|
|
@ -48,3 +44,10 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.btn {
|
||||
/* equal width buttons */
|
||||
flex-basis: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,19 @@ Single.args = {
|
|||
loadpoints: [
|
||||
{
|
||||
title: "Ladepunkt 1",
|
||||
socLevels: [20, 50, 80, 100],
|
||||
mode: "now",
|
||||
socTitle: "Mein Auto",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
hasVehicle: true,
|
||||
charging: true,
|
||||
socCharge: 66,
|
||||
targetSoC: 90,
|
||||
range: 344,
|
||||
chargeEstimate: 999,
|
||||
chargePower: 11232,
|
||||
chargeDuration: 123982,
|
||||
chargedEnergy: 23213,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -45,18 +57,31 @@ export const Multi = Template.bind({});
|
|||
Multi.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
gridPower: 100,
|
||||
gridPower: -100,
|
||||
pvPower: 100,
|
||||
batteryConfigured: true,
|
||||
batteryPower: 100,
|
||||
batterySoC: 0,
|
||||
batterySoC: 60,
|
||||
loadpoints: [
|
||||
{
|
||||
title: "Ladepunkt 1",
|
||||
socLevels: [20, 50, 80, 100],
|
||||
mode: "now",
|
||||
socTitle: "Mein Auto",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
hasVehicle: true,
|
||||
charging: true,
|
||||
socCharge: 66,
|
||||
targetSoC: 90,
|
||||
range: 344,
|
||||
chargeEstimate: 999,
|
||||
chargePower: 11232,
|
||||
chargeDuration: 123982,
|
||||
chargedEnergy: 23213,
|
||||
},
|
||||
{
|
||||
title: "Ladepunkt 2",
|
||||
mode: "pv",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,32 +1,26 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="d-none d-md-flex col-12 col-md-4 mt-md-4 align-items-end">
|
||||
<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">{{ siteTitle || "Home" }}</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 mt-md-4" v-if="multi">
|
||||
<div class="col-12 col-md-9 col-lg-6 flex-grow-1">
|
||||
<SiteDetails v-bind="details"></SiteDetails>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-none d-md-flex border-bottom"></div>
|
||||
|
||||
<div class="row" v-if="!multi">
|
||||
<div class="d-none d-md-block col-md-4"></div>
|
||||
<div class="col-12 col-md-8">
|
||||
<SiteDetails v-bind="details"></SiteDetails>
|
||||
</div>
|
||||
<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">
|
||||
<hr class="w-100 my-4" v-if="id > 0" :key="id + '_hr'" />
|
||||
<Loadpoint
|
||||
:key="loadpoint"
|
||||
v-bind="loadpoint"
|
||||
:single="loadpoints.length === 1"
|
||||
:id="id"
|
||||
:pvConfigured="pvConfigured"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<Loadpoint
|
||||
v-for="(loadpoint, id) in loadpoints"
|
||||
v-bind="loadpoint"
|
||||
:id="id"
|
||||
:key="id"
|
||||
:multi="multi"
|
||||
:pvConfigured="pvConfigured"
|
||||
>
|
||||
</Loadpoint>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -50,6 +44,8 @@ export default {
|
|||
batteryConfigured: Boolean,
|
||||
batteryPower: Number,
|
||||
batterySoC: Number,
|
||||
gridCurrents: Object,
|
||||
prioritySoC: Number,
|
||||
},
|
||||
components: { SiteDetails, Loadpoint },
|
||||
mixins: [formatter, collector],
|
||||
|
|
@ -57,10 +53,6 @@ export default {
|
|||
details: function () {
|
||||
return this.collectProps(SiteDetails);
|
||||
},
|
||||
multi: function () {
|
||||
// TODO fix compact
|
||||
return this.loadpoints.length > 1 /* || app.compact*/;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-3 mt-3" v-if="gridConfigured">
|
||||
<div class="row row-cols-3 justify-content-between justify-content-md-start overflow-hidden">
|
||||
<div class="px-3" v-if="gridConfigured">
|
||||
<div class="mb-2 value" v-if="gridPower > 0">
|
||||
Bezug <fa-icon icon="arrow-down" class="text-primary" />
|
||||
</div>
|
||||
<div class="mb-2 value" v-else>
|
||||
Einspeisung <fa-icon icon="arrow-up" class="text-primary"></fa-icon>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
<h3 class="value">
|
||||
{{ fmt(gridPower) }}
|
||||
<small class="text-muted">{{ fmtUnit(gridPower) }}W</small>
|
||||
</h2>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 mt-3" v-if="pvConfigured">
|
||||
|
||||
<div class="px-3" v-if="pvConfigured">
|
||||
<div class="mb-2 value">
|
||||
Erzeugung
|
||||
<fa-icon
|
||||
|
|
@ -23,22 +24,25 @@
|
|||
}"
|
||||
></fa-icon>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
<h3 class="value">
|
||||
{{ fmt(pvPower) }}
|
||||
<small class="text-muted">{{ fmtUnit(pvPower) }}W</small>
|
||||
</h2>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="d-md-block col-6 col-md-3 mt-3" v-if="batteryConfigured">
|
||||
<div class="px-3" v-if="batteryConfigured">
|
||||
<div class="mb-2 value">
|
||||
Batterie
|
||||
<fa-icon class="text-primary" :icon="batteryIcon"></fa-icon>
|
||||
<div class="d-block d-sm-none">
|
||||
Akku <span class="text-muted"> / {{ batterySoC }} %</span>
|
||||
</div>
|
||||
<div class="d-none d-sm-block">
|
||||
Batterie <span class="text-muted"> / {{ batterySoC }}% </span>
|
||||
<fa-icon class="text-primary" :icon="batteryIcon"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="value">
|
||||
<h3 class="value">
|
||||
{{ fmt(batteryPower) }}
|
||||
<small class="text-muted">{{ fmtUnit(batteryPower) }}W</small>
|
||||
<small class="text-muted">/</small>
|
||||
{{ batterySoC }} <small class="text-muted">%</small>
|
||||
</h2>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -74,6 +78,13 @@ export default {
|
|||
},
|
||||
mixins: [formatter],
|
||||
computed: {
|
||||
numberOfPanels: function () {
|
||||
let count = 0;
|
||||
if (this.gridConfigured) count++;
|
||||
if (this.pvConfigured) count++;
|
||||
if (this.batteryConfigured) count++;
|
||||
return count;
|
||||
},
|
||||
batteryIcon: function () {
|
||||
if (Math.abs(this.batteryPower) < limit) {
|
||||
if (this.batterySoC < 30) return icons[0];
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import Soc from "./Soc.vue";
|
||||
|
||||
export default {
|
||||
title: "Main/Soc",
|
||||
component: Soc,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { Soc },
|
||||
template: '<Soc v-bind="$props"></Soc>',
|
||||
});
|
||||
|
||||
export const Base = Template.bind({});
|
||||
Base.args = {
|
||||
soc: 80,
|
||||
levels: [20, 50, 80, 100],
|
||||
};
|
||||
|
||||
export const Caption = Template.bind({});
|
||||
Caption.args = {
|
||||
caption: true,
|
||||
soc: 80,
|
||||
levels: [20, 50, 80, 100],
|
||||
};
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
<template>
|
||||
<div class="btn-group btn-group-toggle bg-white shadow-none">
|
||||
<label class="btn btn-outline-primary disabled caption font-weight-bold" v-if="caption">
|
||||
Ladeziel
|
||||
</label>
|
||||
<label
|
||||
class="btn btn-outline-primary"
|
||||
v-for="(level, id) in levelsOrDefault"
|
||||
:key="id"
|
||||
:level="level"
|
||||
:id="id"
|
||||
:class="{ active: soc == level, first: !caption && id == 0 }"
|
||||
>
|
||||
<input type="radio" :value="level" v-on:click="setTargetSoC(level)" />{{ level }}%
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Soc",
|
||||
props: {
|
||||
soc: Number,
|
||||
caption: Boolean,
|
||||
levels: Array,
|
||||
},
|
||||
computed: {
|
||||
levelsOrDefault: function () {
|
||||
if (this.levels == null || this.levels.length == 0) {
|
||||
return []; // disabled, or use 30, 50, 80, 100
|
||||
}
|
||||
return this.levels;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setTargetSoC: function (mode) {
|
||||
this.$emit("updated", mode);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -30,6 +30,7 @@ Connected.args = {
|
|||
hasVehicle: true,
|
||||
charging: false,
|
||||
socCharge: 66,
|
||||
targetSoC: 90,
|
||||
};
|
||||
|
||||
export const ReadyToCharge = Template.bind({});
|
||||
|
|
@ -40,6 +41,7 @@ ReadyToCharge.args = {
|
|||
hasVehicle: true,
|
||||
charging: false,
|
||||
socCharge: 66,
|
||||
targetSoC: 90,
|
||||
};
|
||||
|
||||
export const Charging = Template.bind({});
|
||||
|
|
@ -50,6 +52,7 @@ Charging.args = {
|
|||
hasVehicle: true,
|
||||
charging: true,
|
||||
socCharge: 66,
|
||||
targetSoC: 90,
|
||||
};
|
||||
|
||||
const hoursFromNow = function (hours) {
|
||||
|
|
@ -97,6 +100,7 @@ MinCharge.args = {
|
|||
socCharge: 17,
|
||||
minSoC: 20,
|
||||
charging: true,
|
||||
targetSoC: 90,
|
||||
};
|
||||
|
||||
export const UnknownVehicleConnected = Template.bind({});
|
||||
|
|
|
|||
|
|
@ -1,189 +1,48 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="mb-2 pb-1">
|
||||
<div class="mb-3">
|
||||
{{ socTitle || "Fahrzeug" }}
|
||||
</div>
|
||||
<div class="soc-bar">
|
||||
<div class="progress small">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:class="{
|
||||
'progress-bar-striped': charging,
|
||||
'progress-bar-animated': charging,
|
||||
[progressColor]: true,
|
||||
}"
|
||||
:style="{ width: `${socChargeDisplayWidth}%` }"
|
||||
>
|
||||
{{ socChargeDisplayValue }}
|
||||
</div>
|
||||
<div
|
||||
v-if="remainingSoCWidth > 0"
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:class="{
|
||||
'progress-bar-striped': charging,
|
||||
'progress-bar-animated': charging,
|
||||
[progressColor]: true,
|
||||
'bg-muted': true,
|
||||
}"
|
||||
:style="{ width: `${remainingSoCWidth}%` }"
|
||||
></div>
|
||||
<div
|
||||
v-if="socMarker"
|
||||
class="soc-marker"
|
||||
:class="{ [progressColor]: true }"
|
||||
:style="{ left: `${socMarker}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<small v-if="markerLabel()" class="subline my-2 text-secondary">
|
||||
<fa-icon
|
||||
v-if="minSoCActive"
|
||||
class="text-muted mr-1"
|
||||
icon="exclamation-circle"
|
||||
></fa-icon>
|
||||
<fa-icon v-else-if="targetChargeEnabled" class="text-muted mr-1" icon="clock"></fa-icon>
|
||||
{{ markerLabel() }}
|
||||
</small>
|
||||
<VehicleSoc v-bind="vehicleSoc" @target-soc-updated="targetSocUpdated" />
|
||||
<VehicleSubline v-bind="vehicleSubline" class="my-1" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import formatter from "../mixins/formatter";
|
||||
import collector from "../mixins/collector";
|
||||
|
||||
import VehicleSoc from "./VehicleSoc";
|
||||
import VehicleSubline from "./VehicleSubline";
|
||||
|
||||
export default {
|
||||
name: "Vehicle",
|
||||
components: { VehicleSoc, VehicleSubline },
|
||||
props: {
|
||||
socTitle: String,
|
||||
connected: Boolean,
|
||||
hasVehicle: Boolean,
|
||||
socCharge: Number,
|
||||
enabled: Boolean,
|
||||
charging: Boolean,
|
||||
minSoC: Number,
|
||||
socTitle: String,
|
||||
timerActive: Boolean,
|
||||
timerSet: Boolean,
|
||||
targetTime: String,
|
||||
targetSoC: Number,
|
||||
},
|
||||
computed: {
|
||||
socChargeDisplayWidth: function () {
|
||||
if (this.hasVehicle && this.socCharge >= 0) {
|
||||
return this.socCharge;
|
||||
}
|
||||
return 100;
|
||||
vehicleSoc: function () {
|
||||
return this.collectProps(VehicleSoc);
|
||||
},
|
||||
socChargeDisplayValue: function () {
|
||||
// no soc or no soc value
|
||||
if (!this.hasVehicle || !this.socCharge || this.socCharge < 0) {
|
||||
let chargeStatus = "getrennt";
|
||||
if (this.charging) {
|
||||
chargeStatus = "lädt";
|
||||
} else if (this.enabled) {
|
||||
chargeStatus = "bereit";
|
||||
} else if (this.connected) {
|
||||
chargeStatus = "verbunden";
|
||||
}
|
||||
return chargeStatus;
|
||||
}
|
||||
|
||||
// percent value if enough space
|
||||
let socCharge = this.socCharge;
|
||||
if (socCharge >= 10) {
|
||||
socCharge += "%";
|
||||
}
|
||||
return socCharge;
|
||||
},
|
||||
socMarker: function () {
|
||||
if (this.minSoCActive) {
|
||||
return this.minSoC;
|
||||
}
|
||||
if (this.targetSoC > this.socCharge) {
|
||||
return this.targetSoC;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
progressColor: function () {
|
||||
if (!this.connected) {
|
||||
return "bg-light border";
|
||||
}
|
||||
if (this.minSoCActive) {
|
||||
return "bg-danger";
|
||||
}
|
||||
if (this.enabled) {
|
||||
return "bg-primary";
|
||||
}
|
||||
return "bg-secondary";
|
||||
},
|
||||
minSoCActive: function () {
|
||||
return this.minSoC > 0 && this.socCharge < this.minSoC;
|
||||
},
|
||||
targetChargeEnabled: function () {
|
||||
return this.targetTime && this.timerSet;
|
||||
},
|
||||
remainingSoCWidth: function () {
|
||||
if (this.socCharge === 100) {
|
||||
return null;
|
||||
}
|
||||
if (this.minSoCActive) {
|
||||
return this.minSoC - this.socCharge;
|
||||
}
|
||||
if (this.targetSoC > this.socCharge) {
|
||||
return this.targetSoC - this.socCharge;
|
||||
}
|
||||
return null;
|
||||
vehicleSubline: function () {
|
||||
return this.collectProps(VehicleSubline);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// not computed because it needs to update over time
|
||||
markerLabel: function () {
|
||||
if (!this.connected) {
|
||||
return null;
|
||||
}
|
||||
if (this.minSoCActive) {
|
||||
return `Mindestladung bis ${this.socMarker}%`;
|
||||
}
|
||||
if (this.targetChargeEnabled) {
|
||||
const targetDate = Date.parse(this.targetTime);
|
||||
if (this.timerActive) {
|
||||
return `Lädt ${this.fmtRelativeTime(targetDate)} bis ${this.socMarker}%`;
|
||||
} else {
|
||||
return `Geplant bis ${this.fmtAbsoluteDate(targetDate)} bis ${this.socMarker}%`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
targetSocUpdated: function (targetSoC) {
|
||||
this.$emit("target-soc-updated", targetSoC);
|
||||
},
|
||||
},
|
||||
mixins: [formatter],
|
||||
mixins: [collector],
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.subline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.soc-bar {
|
||||
position: relative;
|
||||
height: 31px;
|
||||
}
|
||||
.progress {
|
||||
height: 100%;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.progress-bar.bg-muted {
|
||||
color: var(--white);
|
||||
}
|
||||
.soc-marker {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
width: 2px;
|
||||
}
|
||||
.bg-disabled {
|
||||
background-color: var(--gray);
|
||||
}
|
||||
.bg-light {
|
||||
color: var(--dark);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
251
assets/js/components/VehicleSoc.vue
Normal file
251
assets/js/components/VehicleSoc.vue
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
<template>
|
||||
<div class="vehicle-soc">
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:class="{
|
||||
'progress-bar-striped': charging,
|
||||
'progress-bar-animated': charging,
|
||||
[progressColor]: true,
|
||||
}"
|
||||
:style="{ width: `${socChargeDisplayWidth}%` }"
|
||||
>
|
||||
{{ socChargeDisplayValue }}
|
||||
</div>
|
||||
<div
|
||||
v-if="remainingSoCWidth > 0"
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:class="{
|
||||
[progressColor]: true,
|
||||
'bg-muted': true,
|
||||
}"
|
||||
:style="{ width: `${remainingSoCWidth}%`, transition: 'none' }"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasVehicle && visibleTargetSoC"
|
||||
class="target"
|
||||
:class="{ 'target--max': visibleTargetSoC === 100 }"
|
||||
>
|
||||
<div
|
||||
class="target-label d-flex align-items-center justify-content-center"
|
||||
:style="{ left: `${visibleTargetSoC}%` }"
|
||||
>
|
||||
{{ visibleTargetSoC }}%
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="5"
|
||||
:value="visibleTargetSoC"
|
||||
class="target-slider"
|
||||
@input="movedTargetSoC"
|
||||
@change="setTargetSoC"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "VehicleSoc",
|
||||
props: {
|
||||
connected: Boolean,
|
||||
hasVehicle: Boolean,
|
||||
socCharge: Number,
|
||||
enabled: Boolean,
|
||||
charging: Boolean,
|
||||
minSoC: Number,
|
||||
targetSoC: Number,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
selectedTargetSoC: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
socChargeDisplayWidth: function () {
|
||||
if (this.hasVehicle && this.socCharge >= 0) {
|
||||
return this.socCharge;
|
||||
}
|
||||
return 100;
|
||||
},
|
||||
socChargeDisplayValue: function () {
|
||||
// no soc or no soc value
|
||||
if (!this.hasVehicle || !this.socCharge || this.socCharge < 0) {
|
||||
let chargeStatus = "getrennt";
|
||||
if (this.charging) {
|
||||
chargeStatus = "lädt";
|
||||
} else if (this.enabled) {
|
||||
chargeStatus = "bereit";
|
||||
} else if (this.connected) {
|
||||
chargeStatus = "verbunden";
|
||||
}
|
||||
return chargeStatus;
|
||||
}
|
||||
|
||||
// percent value if enough space
|
||||
let socCharge = this.socCharge;
|
||||
if (socCharge >= 10) {
|
||||
socCharge += "%";
|
||||
}
|
||||
return socCharge;
|
||||
},
|
||||
progressColor: function () {
|
||||
if (!this.connected) {
|
||||
return "bg-light border";
|
||||
}
|
||||
if (this.minSoCActive) {
|
||||
return "bg-danger";
|
||||
}
|
||||
if (this.enabled) {
|
||||
return "bg-primary";
|
||||
}
|
||||
return "bg-secondary";
|
||||
},
|
||||
minSoCActive: function () {
|
||||
return this.minSoC > 0 && this.socCharge < this.minSoC;
|
||||
},
|
||||
remainingSoCWidth: function () {
|
||||
if (this.socChargeDisplayWidth === 100) {
|
||||
return null;
|
||||
}
|
||||
if (this.minSoCActive) {
|
||||
return this.minSoC - this.socCharge;
|
||||
}
|
||||
if (this.visibleTargetSoC > this.socCharge) {
|
||||
return this.visibleTargetSoC - this.socCharge;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
visibleTargetSoC: function () {
|
||||
return Number(this.selectedTargetSoC || this.targetSoC);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
movedTargetSoC: function (e) {
|
||||
const minTargetSoC = 40;
|
||||
if (e.target.value < minTargetSoC) {
|
||||
e.target.value = minTargetSoC;
|
||||
this.selectedTargetSoC = e.target.value;
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
this.selectedTargetSoC = e.target.value;
|
||||
return true;
|
||||
},
|
||||
setTargetSoC: function (e) {
|
||||
this.$emit("target-soc-updated", e.target.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.vehicle-soc {
|
||||
--height: 38px;
|
||||
--thumb-overlap: 3px;
|
||||
--thumb-width: 3px;
|
||||
--thumb-horizontal-padding: 15px;
|
||||
--label-height: 26px;
|
||||
position: relative;
|
||||
height: var(--height);
|
||||
}
|
||||
.progress {
|
||||
height: 100%;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.progress-bar.bg-muted {
|
||||
color: var(--white);
|
||||
}
|
||||
.bg-light {
|
||||
color: var(--dark);
|
||||
}
|
||||
.target-label {
|
||||
width: 3em;
|
||||
margin-left: -1.5em;
|
||||
height: var(--label-height);
|
||||
position: absolute;
|
||||
top: calc((var(--label-height) + var(--thumb-overlap)) * -1);
|
||||
text-align: center;
|
||||
color: var(--dark);
|
||||
font-size: 1rem;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease 1s;
|
||||
}
|
||||
.target-slider {
|
||||
-webkit-appearance: none;
|
||||
position: absolute;
|
||||
top: calc(var(--thumb-overlap) * -1);
|
||||
left: calc(var(--thumb-horizontal-padding) * -1);
|
||||
height: calc(100% + 2 * var(--thumb-overlap));
|
||||
width: calc(100% + 2 * var(--thumb-horizontal-padding));
|
||||
background: transparent;
|
||||
}
|
||||
.target-slider:focus {
|
||||
outline: none;
|
||||
}
|
||||
/* Note: Safari,Chrome,Blink and Firefox specific styles need to be in separate definitions to work */
|
||||
.target-slider::-webkit-slider-runnable-track {
|
||||
background: transparent;
|
||||
border: none;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.target-slider::-moz-range-track {
|
||||
background: transparent;
|
||||
border: none;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.target-slider::-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(--dark);
|
||||
cursor: grab;
|
||||
border: none;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease 1s;
|
||||
}
|
||||
.target-slider::-moz-range-thumb {
|
||||
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(--dark);
|
||||
cursor: grab;
|
||||
border: none;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease 1s;
|
||||
}
|
||||
/* auto-hide targetSoC marker at 100% */
|
||||
.target--max .target-slider::-webkit-slider-thumb,
|
||||
.target--max .target-label {
|
||||
opacity: 0;
|
||||
}
|
||||
.target--max .target-slider::-moz-range-thumb,
|
||||
.target--max .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>
|
||||
53
assets/js/components/VehicleSubline.vue
Normal file
53
assets/js/components/VehicleSubline.vue
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-secondary">
|
||||
<span v-if="minSoCActive">
|
||||
<fa-icon class="text-muted mr-1" icon="exclamation-circle"></fa-icon>
|
||||
Mindestladung bis {{ minSoC }}%
|
||||
</span>
|
||||
</small>
|
||||
<small
|
||||
v-if="targetChargeEnabled"
|
||||
:class="{
|
||||
invisible: !targetSoC,
|
||||
'text-primary': timerActive,
|
||||
'text-secondary': !timerActive,
|
||||
}"
|
||||
>
|
||||
{{ targetTimeLabel() }}
|
||||
<fa-icon class="ml-1" icon="clock"></fa-icon>
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import formatter from "../mixins/formatter";
|
||||
|
||||
export default {
|
||||
name: "VehicleSubline",
|
||||
props: {
|
||||
socCharge: Number,
|
||||
minSoC: Number,
|
||||
timerActive: Boolean,
|
||||
timerSet: Boolean,
|
||||
targetTime: String,
|
||||
targetSoC: Number,
|
||||
},
|
||||
computed: {
|
||||
minSoCActive: function () {
|
||||
return this.minSoC > 0 && this.socCharge < this.minSoC;
|
||||
},
|
||||
targetChargeEnabled: function () {
|
||||
return this.targetTime && this.timerSet;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// not computed because it needs to update over time
|
||||
targetTimeLabel: function () {
|
||||
const targetDate = new Date(this.targetTime);
|
||||
return `bis ${this.fmtAbsoluteDate(targetDate)} Uhr`;
|
||||
},
|
||||
},
|
||||
mixins: [formatter],
|
||||
};
|
||||
</script>
|
||||
|
|
@ -58,34 +58,23 @@ export default {
|
|||
}
|
||||
return "m";
|
||||
},
|
||||
fmtDayString: function (date) {
|
||||
const YY = `${date.getFullYear()}`;
|
||||
const MM = `${date.getMonth() + 1}`.padStart(2, "0");
|
||||
const DD = `${date.getDate()}`.padStart(2, "0");
|
||||
return `${YY}-${MM}-${DD}`;
|
||||
},
|
||||
fmtTimeString: function (date) {
|
||||
const HH = `${date.getHours()}`.padStart(2, "0");
|
||||
const mm = `${date.getMinutes()}`.padStart(2, "0");
|
||||
return `${HH}:${mm}`;
|
||||
},
|
||||
fmtAbsoluteDate: function (date) {
|
||||
return new Intl.DateTimeFormat("de-DE", {
|
||||
return new Intl.DateTimeFormat("default", {
|
||||
weekday: "short",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
}).format(date);
|
||||
},
|
||||
fmtRelativeTime: function (date) {
|
||||
var units = {
|
||||
year: 24 * 60 * 60 * 1000 * 365,
|
||||
month: (24 * 60 * 60 * 1000 * 365) / 12,
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
hour: 60 * 60 * 1000,
|
||||
minute: 60 * 1000,
|
||||
second: 1000,
|
||||
};
|
||||
|
||||
var rtf = new Intl.RelativeTimeFormat("de", { numeric: "auto", style: "narrow" });
|
||||
|
||||
var getRelativeTime = (d1, d2 = new Date()) => {
|
||||
var elapsed = d1 - d2;
|
||||
|
||||
// "Math.abs" accounts for both "past" & "future" scenarios
|
||||
for (var u in units)
|
||||
if (Math.abs(elapsed) > units[u] || u == "second")
|
||||
return rtf.format(Math.round(elapsed / units[u]), u);
|
||||
};
|
||||
return getRelativeTime(date);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="app d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div class="app d-flex flex-column justify-content-between overflow-hidden">
|
||||
<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">
|
||||
|
|
@ -32,7 +32,9 @@
|
|||
</div>
|
||||
</nav>
|
||||
|
||||
<router-view></router-view>
|
||||
<router-view
|
||||
class="flex-grow-1 d-flex flex-column justify-content-stretch"
|
||||
></router-view>
|
||||
</div>
|
||||
<Footer :version="version"></Footer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -42,7 +41,6 @@ type PollConfig struct {
|
|||
type SoCConfig struct {
|
||||
Poll PollConfig `mapstructure:"poll"`
|
||||
AlwaysUpdate bool `mapstructure:"alwaysUpdate"`
|
||||
Levels []int `mapstructure:"levels"`
|
||||
Estimate bool `mapstructure:"estimate"`
|
||||
Min int `mapstructure:"min"` // Default minimum SoC, guarded by mutex
|
||||
Target int `mapstructure:"target"` // Default target SoC, guarded by mutex
|
||||
|
|
@ -135,8 +133,6 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
|
|||
lp.Mode = api.ChargeModeString(string(lp.Mode))
|
||||
lp.OnDisconnect.Mode = api.ChargeModeString(string(lp.OnDisconnect.Mode))
|
||||
|
||||
sort.Ints(lp.SoC.Levels)
|
||||
|
||||
// set vehicle polling mode
|
||||
switch lp.SoC.Poll.Mode = strings.ToLower(lp.SoC.Poll.Mode); lp.SoC.Poll.Mode {
|
||||
case pollCharging:
|
||||
|
|
@ -167,10 +163,6 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
|
|||
if lp.SoC.Target == 0 {
|
||||
lp.SoC.Target = 100
|
||||
}
|
||||
|
||||
if len(lp.SoC.Levels) > 0 {
|
||||
lp.SoC.Target = lp.SoC.Levels[len(lp.SoC.Levels)-1]
|
||||
}
|
||||
}
|
||||
|
||||
if lp.Meters.ChargeMeterRef != "" {
|
||||
|
|
@ -411,7 +403,6 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
|
|||
lp.publish("mode", lp.Mode)
|
||||
lp.publish("targetSoC", lp.SoC.Target)
|
||||
lp.publish("minSoC", lp.SoC.Min)
|
||||
lp.publish("socLevels", lp.SoC.Levels)
|
||||
lp.Unlock()
|
||||
|
||||
// use first vehicle for estimator
|
||||
|
|
|
|||
1
dist/css/index.3b453e73.css
vendored
1
dist/css/index.3b453e73.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}.fade-enter-active[data-v-a6ded36e],.fade-leave-active[data-v-a6ded36e]{transition:opacity .25s ease-in}.fade-enter[data-v-a6ded36e],.fade-leave-to[data-v-a6ded36e]{opacity:0}.dialog[data-v-a6ded36e]{position:fixed;top:0;left:0;z-index:1050;width:100%;height:100%;overflow:hidden;outline:0}.icon[data-v-a6ded36e]{color:#0fdd42}.text-black a[data-v-a6ded36e]{color:#18191a}.icon[data-v-5f0f38d1]{color:#0fdd42}.text-black a[data-v-5f0f38d1]{color:#18191a}.logo[data-v-459f11b2]{width:85px}.app[data-v-459f11b2]{min-height:100vh}.subline[data-v-b9f97a64]{display:flex;align-items:center}.soc-bar[data-v-b9f97a64]{position:relative;height:31px}.progress[data-v-b9f97a64]{height:100%;font-size:.875rem}.progress-bar.bg-muted[data-v-b9f97a64]{color:var(--white)}.soc-marker[data-v-b9f97a64]{position:absolute;top:-2px;bottom:-2px;width:2px}.bg-disabled[data-v-b9f97a64]{background-color:var(--gray)}.bg-light[data-v-b9f97a64]{color:var(--dark)}
|
||||
1
dist/css/index.9b973d78.css
vendored
Normal file
1
dist/css/index.9b973d78.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.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}.fade-enter-active[data-v-a6ded36e],.fade-leave-active[data-v-a6ded36e]{transition:opacity .25s ease-in}.fade-enter[data-v-a6ded36e],.fade-leave-to[data-v-a6ded36e]{opacity:0}.dialog[data-v-a6ded36e]{position:fixed;top:0;left:0;z-index:1050;width:100%;height:100%;overflow:hidden;outline:0}.icon[data-v-a6ded36e]{color:#0fdd42}.text-black a[data-v-a6ded36e]{color:#18191a}.icon[data-v-5f0f38d1]{color:#0fdd42}.text-black a[data-v-5f0f38d1]{color:#18191a}.logo[data-v-8dad761a]{width:85px}.app[data-v-8dad761a]{min-height:100vh}.btn[data-v-c8c77010]{flex-basis:0}.vehicle-soc[data-v-7f94c1fe]{--height:38px;--thumb-overlap:3px;--thumb-width:3px;--thumb-horizontal-padding:15px;--label-height:26px;position:relative;height:var(--height)}.progress[data-v-7f94c1fe]{height:100%;font-size:1rem}.progress-bar.bg-muted[data-v-7f94c1fe]{color:var(--white)}.bg-light[data-v-7f94c1fe]{color:var(--dark)}.target-label[data-v-7f94c1fe]{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(--dark);font-size:1rem;opacity:1;transition:opacity .2s ease 1s}.target-slider[data-v-7f94c1fe]{-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-7f94c1fe]:focus{outline:none}.target-slider[data-v-7f94c1fe]::-webkit-slider-runnable-track{background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-7f94c1fe]::-moz-range-track{background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-7f94c1fe]::-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(--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-7f94c1fe]::-moz-range-thumb{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(--dark);cursor:grab;border:none;opacity:1;-moz-transition:opacity .2s ease 1s;transition:opacity .2s ease 1s}.target--max .target-label[data-v-7f94c1fe],.target--max .target-slider[data-v-7f94c1fe]::-webkit-slider-thumb{opacity:0}.target--max .target-label[data-v-7f94c1fe],.target--max .target-slider[data-v-7f94c1fe]::-moz-range-thumb{opacity:0}.target:hover .target-label[data-v-7f94c1fe],.target:hover .target-slider[data-v-7f94c1fe]::-webkit-slider-thumb{opacity:1;transition-delay:0s}.target:hover .target-label[data-v-7f94c1fe],.target:hover .target-slider[data-v-7f94c1fe]::-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="en"><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.52eaf30c.css" rel="preload" as="style"><link href="css/index.3b453e73.css" rel="preload" as="style"><link href="js/chunk-vendors.35669cb4.js" rel="preload" as="script"><link href="js/index.eb5cb609.js" rel="preload" as="script"><link href="css/chunk-vendors.52eaf30c.css" rel="stylesheet"><link href="css/index.3b453e73.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.52eaf30c.css" rel="preload" as="style"><link href="css/index.9b973d78.css" rel="preload" as="style"><link href="js/chunk-vendors.35669cb4.js" rel="preload" as="script"><link href="js/index.9bd6ba1b.js" rel="preload" as="script"><link href="css/chunk-vendors.52eaf30c.css" rel="stylesheet"><link href="css/index.9b973d78.css" rel="stylesheet"></head><body><script>window.evcc = {
|
||||
version: "[[.Version]]",
|
||||
configured: "[[.Configured]]",
|
||||
};</script><div id="app"></div><div id="toasts"></div><script src="js/chunk-vendors.35669cb4.js"></script><script src="js/index.eb5cb609.js"></script></body></html>
|
||||
};</script><div id="app"></div><div id="toasts"></div><script src="js/chunk-vendors.35669cb4.js"></script><script src="js/index.9bd6ba1b.js"></script></body></html>
|
||||
2
dist/js/index.9bd6ba1b.js
vendored
Normal file
2
dist/js/index.9bd6ba1b.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/js/index.9bd6ba1b.js.map
vendored
Normal file
1
dist/js/index.9bd6ba1b.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/js/index.eb5cb609.js
vendored
2
dist/js/index.eb5cb609.js
vendored
File diff suppressed because one or more lines are too long
1
dist/js/index.eb5cb609.js.map
vendored
1
dist/js/index.eb5cb609.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -89,11 +89,6 @@ loadpoints:
|
|||
min: 0 # immediately charge to 0% regardless of mode unless "off" (disabled)
|
||||
target: 100 # always charge to 100%
|
||||
estimate: false # set true to interpolate between api updates
|
||||
levels: # target soc levels for UI
|
||||
- 30
|
||||
- 50
|
||||
- 80
|
||||
- 100
|
||||
onDisconnect: # set defaults when vehicle disconnects
|
||||
mode: pv # switch back to pv mode
|
||||
targetSoC: 100 # charge to 100%
|
||||
|
|
|
|||
|
|
@ -122,12 +122,6 @@
|
|||
"target": {
|
||||
"type": "integer"
|
||||
},
|
||||
"levels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"estimate": {
|
||||
"type": "boolean"
|
||||
}
|
||||
|
|
@ -175,4 +169,4 @@
|
|||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -292,7 +292,7 @@ func TargetChargeHandler(loadpoint core.LoadPointAPI) http.HandlerFunc {
|
|||
timeS, ok := vars["time"]
|
||||
timeV, err := time.ParseInLocation("2006-01-02T15:04:05", timeS, timezone())
|
||||
|
||||
if !ok || err != nil || timeV.Before(time.Now()) {
|
||||
if !ok || err != nil {
|
||||
log.DEBUG.Printf("parse time: %v", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue