Target Charge UI (#1434)

added target charge ui (experimental)
This commit is contained in:
Michael Geers 2021-08-25 21:00:45 +02:00 • committed by GitHub
parent 7359a54e5a
commit e4fe538b79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 311 additions and 33 deletions

View file

@ -8,12 +8,17 @@
--evcc-export: var(--evcc-yellow);
--bs-primary: var(--evcc-dark-green);
--bs-primary-rgb: 58, 186, 44;
}
.bg-primary {
background-color: var(--evcc-dark-green) !important;
}
a {
color: var(--evcc-dark-green);
}
/* reverse loading animation */
.progress-bar-animated {
animation-direction: reverse;
@ -21,3 +26,12 @@
.bg-muted {
opacity: 0.25;
}
.btn-primary {
background-color: var(--bs-gray-dark);
border-color: var(--bs-gray-dark);
}
.btn-primary:hover {
background-color: var(--evcc-dark-green);
border-color: var(--evcc-dark-green);
}

View file

@ -16,6 +16,7 @@
class="col-12 col-md-6 col-lg-8 mb-4"
v-bind="vehicle"
@target-soc-updated="setTargetSoC"
@target-time-updated="setTargetTime"
/>
</div>
<LoadpointDetails v-bind="details" />
@ -134,6 +135,12 @@ export default {
)
.catch(window.app.error);
},
setTargetTime: function (date) {
const formattedDate = `${this.fmtDayString(date)}T${this.fmtTimeString(date)}:00`;
axios
.post(this.api("targetcharge") + "/" + this.targetSoC + "/" + formattedDate)
.catch(window.app.error);
},
},
destroyed: function () {
window.clearInterval(this.tickerHandle);

View file

@ -0,0 +1,206 @@
<template>
<div>
<button
class="target-time-button btn btn-link btn-sm pe-0"
:class="{
invisible: !targetSoC,
'text-dark': timerActive,
'text-secondary': !timerActive,
}"
data-bs-toggle="modal"
:data-bs-target="`#${modalId}`"
>
{{ targetTimeLabel() }}<fa-icon class="ms-1" icon="clock"></fa-icon>
</button>
<div :id="modalId" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{ $t("main.targetCharge.modalTitle") }}
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<form @submit.prevent="saveTargetTime">
<div class="modal-body">
<div class="form-group">
<label
for="targetTimeLabel"
class="mb-3"
v-html="$t('main.targetCharge.description', { targetSoC })"
>
</label>
<div
class="d-flex justify-content-between"
:style="{ 'max-width': '350px' }"
>
<select
class="form-select me-2"
v-model="selectedDay"
:style="{ 'flex-basis': '60%' }"
>
<option
v-for="opt in dayOptions()"
:value="opt.value"
:key="opt.value"
>
{{ opt.name }}
</option>
</select>
<input
type="time"
class="form-control ms-2"
:style="{ 'flex-basis': '40%' }"
v-model="selectedTime"
:step="60 * 5"
required
/>
</div>
</div>
<p class="text-danger mb-0" v-if="!selectedTargetTimeValid">
{{ $t("main.targetCharge.targetIsInThePast") }}
</p>
<p class="small mt-3 text-muted">
<strong class="text-primary">
<fa-icon icon="flask"></fa-icon>
{{ $t("main.targetCharge.experimentalLabel") }}:
</strong>
{{ $t("main.targetCharge.experimentalText") }}
<a
href="https://github.com/evcc-io/evcc/discussions/1433"
target="_blank"
>GitHub Discussions</a
>.
</p>
</div>
<div class="modal-footer d-flex justify-content-between">
<button
type="button"
class="btn btn-outline-secondary"
data-bs-dismiss="modal"
@click="removeTargetTime"
>
{{ $t("main.targetCharge.remove") }}
</button>
<button
type="submit"
class="btn btn-primary"
data-bs-dismiss="modal"
:disabled="!selectedTargetTimeValid"
>
{{ $t("main.targetCharge.activate") }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
import formatter from "../mixins/formatter";
export default {
name: "TargetCharge",
props: {
id: Number,
timerActive: Boolean,
timerSet: Boolean,
targetTime: String,
targetSoC: Number,
},
computed: {
targetChargeEnabled: function () {
return this.targetTime && this.timerSet;
},
selectedTargetTimeValid: function () {
const now = new Date();
return now < this.selectedTargetTime;
},
selectedTargetTime: function () {
return new Date(`${this.selectedDay}T${this.selectedTime || "00:00"}`);
},
modalId: function () {
return `targetChargeModal_${this.id}`;
},
},
data: function () {
return { selectedDay: null, selectedTime: null };
},
mounted: function () {
this.initInputFields();
},
watch: {
targetTime() {
this.initInputFields();
},
},
methods: {
// not computed because it needs to update over time
targetTimeLabel: function () {
if (this.targetChargeEnabled) {
const targetDate = new Date(this.targetTime);
return this.$t("main.targetCharge.activeLabel", {
time: this.fmtAbsoluteDate(targetDate),
});
}
return this.$t("main.targetCharge.inactiveLabel");
},
defaultDate: function () {
const now = new Date();
// 12 hrs from now
now.setHours(now.getHours() + 12);
// round to quarter hour
now.setMinutes(Math.ceil(now.getMinutes() / 15) * 15);
return now;
},
initInputFields: function () {
let date = this.defaultDate();
let targetTimeInTheFuture = new Date(this.targetTime) > new Date();
if (this.targetChargeEnabled && targetTimeInTheFuture) {
date = new Date(this.targetTime);
}
this.selectedDay = this.fmtDayString(date);
this.selectedTime = this.fmtTimeString(date);
},
dayOptions: function () {
const options = [];
const date = new Date();
const labels = [
this.$t("main.targetCharge.today"),
this.$t("main.targetCharge.tomorrow"),
];
for (let i = 0; i < 7; i++) {
const dayNumber = date.toLocaleDateString("default", {
month: "long",
day: "numeric",
});
const dayName =
labels[i] || date.toLocaleDateString("default", { weekday: "long" });
options.push({
value: date.toISOString().split("T")[0],
name: `${dayNumber} (${dayName})`,
});
date.setDate(date.getDate() + 1);
}
return options;
},
minTime: function () {
return new Date().toISOString().split("T")[1].slice(0, -8);
},
removeTargetTime: function () {
this.$emit("target-time-updated", new Date(null));
},
saveTargetTime: function () {
this.$emit("target-time-updated", this.selectedTargetTime);
},
},
mixins: [formatter],
};
</script>

View file

@ -22,6 +22,7 @@ Base.args = {
vehiclePresent: true,
vehicleSoc: 42,
targetSoC: 90,
id: 0,
};
export const Connected = Template.bind({});
@ -33,6 +34,7 @@ Connected.args = {
charging: false,
vehicleSoc: 66,
targetSoC: 90,
id: 0,
};
export const ReadyToCharge = Template.bind({});
@ -44,6 +46,7 @@ ReadyToCharge.args = {
charging: false,
vehicleSoc: 66,
targetSoC: 90,
id: 0,
};
export const Charging = Template.bind({});
@ -55,6 +58,7 @@ Charging.args = {
charging: true,
vehicleSoc: 66,
targetSoC: 90,
id: 0,
};
const hoursFromNow = function (hours) {
@ -76,6 +80,7 @@ TargetChargePlanned.args = {
timerActive: false,
targetSoC: 45,
targetTime: hoursFromNow(14),
id: 0,
};
export const TargetChargeActive = Template.bind({});
@ -91,6 +96,7 @@ TargetChargeActive.args = {
timerActive: true,
targetSoC: 80,
targetTime: hoursFromNow(2),
id: 0,
};
export const MinCharge = Template.bind({});
@ -103,6 +109,7 @@ MinCharge.args = {
minSoC: 20,
charging: true,
targetSoC: 90,
id: 0,
};
export const UnknownVehicleConnected = Template.bind({});
@ -111,6 +118,7 @@ UnknownVehicleConnected.args = {
enabled: false,
connected: true,
vehiclePresent: false,
id: 0,
};
export const UnknownVehicleReadyToCharge = Template.bind({});
@ -120,6 +128,7 @@ UnknownVehicleReadyToCharge.args = {
connected: true,
vehiclePresent: false,
charging: false,
id: 0,
};
export const UnknownVehicleCharging = Template.bind({});
@ -129,6 +138,7 @@ UnknownVehicleCharging.args = {
connected: true,
vehiclePresent: false,
charging: true,
id: 0,
};
export const Disconnected = Template.bind({});
@ -136,6 +146,7 @@ Disconnected.args = {
vehicleTitle: "Mein Auto",
connected: false,
vehiclePresent: false,
id: 0,
};
export const DisconnectedKnownSoc = Template.bind({});
@ -146,4 +157,5 @@ DisconnectedKnownSoc.args = {
vehiclePresent: true,
vehicleSoc: 17,
targetSoC: 60,
id: 0,
};

View file

@ -4,7 +4,11 @@
{{ vehicleTitle || $t("main.vehicle.fallbackName") }}
</div>
<VehicleSoc v-bind="vehicleSocProps" @target-soc-updated="targetSocUpdated" />
<VehicleSubline v-bind="vehicleSubline" class="my-1" />
<VehicleSubline
v-bind="vehicleSubline"
@target-time-updated="targetTimeUpdated"
class="my-1"
/>
</div>
</template>
@ -18,6 +22,7 @@ export default {
name: "Vehicle",
components: { VehicleSoc, VehicleSubline },
props: {
id: Number,
connected: Boolean,
vehiclePresent: Boolean,
vehicleSoc: Number,
@ -42,6 +47,9 @@ export default {
targetSocUpdated: function (targetSoC) {
this.$emit("target-soc-updated", targetSoC);
},
targetTimeUpdated: function (targetTime) {
this.$emit("target-time-updated", targetTime);
},
},
mixins: [collector],
};

View file

@ -6,26 +6,19 @@
{{ $t("main.vehicleSubline.mincharge", { soc: minSoC }) }}
</span>
</small>
<small
v-if="targetChargeEnabled"
:class="{
invisible: !targetSoC,
'text-primary': timerActive,
'text-secondary': !timerActive,
}"
>
{{ targetTimeLabel() }}
<fa-icon class="ms-1" icon="clock"></fa-icon>
</small>
<TargetCharge v-bind="targetCharge" @target-time-updated="targetTimeUpdated" />
</div>
</template>
<script>
import formatter from "../mixins/formatter";
import collector from "../mixins/collector";
import TargetCharge from "./TargetCharge.vue";
export default {
name: "VehicleSubline",
components: { TargetCharge },
props: {
id: Number,
vehicleSoc: Number,
minSoC: Number,
timerActive: Boolean,
@ -37,17 +30,15 @@ export default {
minSoCActive: function () {
return this.minSoC > 0 && this.vehicleSoc < this.minSoC;
},
targetChargeEnabled: function () {
return this.targetTime && this.timerSet;
targetCharge: function () {
return this.collectProps(TargetCharge);
},
},
mixins: [collector],
methods: {
// not computed because it needs to update over time
targetTimeLabel: function () {
const targetDate = new Date(this.targetTime);
return `bis ${this.fmtAbsoluteDate(targetDate)} Uhr`;
targetTimeUpdated: function (targetTime) {
this.$emit("target-time-updated", targetTime);
},
},
mixins: [formatter],
};
</script>

View file

@ -67,6 +67,25 @@ export default {
vehicleSubline: {
mincharge: "Mindestladung bis {soc}%",
},
targetCharge: {
inactiveLabel: "Zielzeit",
activeLabel: "bis {time} Uhr",
modalTitle: "Zielzeit festlegen",
description: "Wann soll das Fahrzeug auf <strong>{targetSoC}%</strong> geladen sein?",
today: "heute",
tomorrow: "morgen",
targetIsInThePast: "Zeitpunkt liegt in der Vergangenheit.",
remove: "Keine Zielzeit",
activate: "Zielzeit aktivieren",
experimentalLabel: "experimentell",
experimentalText: `
Diese Funktion ist in einem frühen Stadium. Der Algorithmus ist noch
nicht perfekt. Die Zielzeit wird aktuell nicht persistiert - das
heißt sie geht beim Neustart verloren. Verlasse dich also noch nicht
zu sehr auf diese Funktion. Wir freuen uns aber über deine
Erfahrungen und Verbessungsvorschläge in den
`,
},
loadpointDetails: {
power: "Leistung",
range: "Reichweite",

View file

@ -67,6 +67,25 @@ export default {
vehicleSubline: {
mincharge: "minimum charging to {soc}%",
},
targetCharge: {
inactiveLabel: "Target time",
activeLabel: "until {time}",
modalTitle: "Set Target Time",
description: "When should the vehicle be chargerd to <strong>{targetSoC}%</strong>?",
today: "today",
tomorrow: "tomorrow",
targetIsInThePast: "The chosen time is in the past.",
remove: "Remove",
activate: "Activate",
experimentalLabel: "experimental",
experimentalText: `
This function is at an early stage.
The algorithm is not perfect yet.
The target time is currently not persisted - this means, it will be lost when your server restarts.
So do not rely too much on this function.
However, we look forward to your experiences and suggestions for improvement in the
`,
},
loadpointDetails: {
power: "Power",
range: "Range",

View file

@ -12,6 +12,7 @@ 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 { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
import { faFlask } from "@fortawesome/free-solid-svg-icons/faFlask";
import { faTemperatureHigh } from "@fortawesome/free-solid-svg-icons/faTemperatureHigh";
import { faTemperatureLow } from "@fortawesome/free-solid-svg-icons/faTemperatureLow";
import { faThermometerHalf } from "@fortawesome/free-solid-svg-icons/faThermometerHalf";
@ -47,6 +48,7 @@ library.add(
fasHeart,
faSquare,
faSun,
faFlask,
faTemperatureHigh,
faTemperatureLow,
faThermometerHalf

File diff suppressed because one or more lines are too long

1
dist/css/index.93d569c8.css vendored Normal file

File diff suppressed because one or more lines are too long

4
dist/index.html vendored
View file

@ -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.63c5e34a.css" rel="preload" as="style"><link href="css/index.610b9fdd.css" rel="preload" as="style"><link href="js/chunk-vendors.255a20c6.js" rel="preload" as="script"><link href="js/index.e00fd359.js" rel="preload" as="script"><link href="css/chunk-vendors.63c5e34a.css" rel="stylesheet"><link href="css/index.610b9fdd.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.63c5e34a.css" rel="preload" as="style"><link href="css/index.93d569c8.css" rel="preload" as="style"><link href="js/chunk-vendors.0dc6e0ff.js" rel="preload" as="script"><link href="js/index.71929600.js" rel="preload" as="script"><link href="css/chunk-vendors.63c5e34a.css" rel="stylesheet"><link href="css/index.93d569c8.css" rel="stylesheet"></head><body><script>window.evcc = {
version: "[[.Version]]",
configured: "[[.Configured]]",
};</script><div id="app"></div><script src="js/chunk-vendors.255a20c6.js"></script><script src="js/index.e00fd359.js"></script></body></html>
};</script><div id="app"></div><script src="js/chunk-vendors.0dc6e0ff.js"></script><script src="js/index.71929600.js"></script></body></html>

File diff suppressed because one or more lines are too long

1
dist/js/chunk-vendors.0dc6e0ff.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/js/index.71929600.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/js/index.71929600.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long