Add target energy API & UI for vehicles without SoC
This commit is contained in:
parent
0ca26eafa2
commit
26f149571c
24 changed files with 664 additions and 192 deletions
|
|
@ -221,3 +221,9 @@ type AuthProvider interface {
|
|||
LoginHandler() http.HandlerFunc
|
||||
LogoutHandler() http.HandlerFunc
|
||||
}
|
||||
|
||||
// FeatureDescriber optionally provides a list of supported non-api features
|
||||
type FeatureDescriber interface {
|
||||
Features() []Feature
|
||||
Has(Feature) bool
|
||||
}
|
||||
|
|
|
|||
17
api/feature.go
Normal file
17
api/feature.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package api
|
||||
|
||||
type Feature int
|
||||
|
||||
func (f *Feature) UnmarshalText(text []byte) error {
|
||||
feat, err := FeatureString(string(text))
|
||||
if err == nil {
|
||||
*f = feat
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
//go:generate enumer -type Feature
|
||||
const (
|
||||
_ Feature = iota
|
||||
Offline
|
||||
)
|
||||
75
api/feature_enumer.go
Normal file
75
api/feature_enumer.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Code generated by "enumer -type Feature"; DO NOT EDIT.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _FeatureName = "Offline"
|
||||
|
||||
var _FeatureIndex = [...]uint8{0, 7}
|
||||
|
||||
const _FeatureLowerName = "offline"
|
||||
|
||||
func (i Feature) String() string {
|
||||
i -= 1
|
||||
if i < 0 || i >= Feature(len(_FeatureIndex)-1) {
|
||||
return fmt.Sprintf("Feature(%d)", i+1)
|
||||
}
|
||||
return _FeatureName[_FeatureIndex[i]:_FeatureIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _FeatureNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[Offline-(1)]
|
||||
}
|
||||
|
||||
var _FeatureValues = []Feature{Offline}
|
||||
|
||||
var _FeatureNameToValueMap = map[string]Feature{
|
||||
_FeatureName[0:7]: Offline,
|
||||
_FeatureLowerName[0:7]: Offline,
|
||||
}
|
||||
|
||||
var _FeatureNames = []string{
|
||||
_FeatureName[0:7],
|
||||
}
|
||||
|
||||
// FeatureString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func FeatureString(s string) (Feature, error) {
|
||||
if val, ok := _FeatureNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _FeatureNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to Feature values", s)
|
||||
}
|
||||
|
||||
// FeatureValues returns all values of the enum
|
||||
func FeatureValues() []Feature {
|
||||
return _FeatureValues
|
||||
}
|
||||
|
||||
// FeatureStrings returns a slice of all String values of the enum
|
||||
func FeatureStrings() []string {
|
||||
strs := make([]string, len(_FeatureNames))
|
||||
copy(strs, _FeatureNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAFeature returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i Feature) IsAFeature() bool {
|
||||
for _, v := range _FeatureValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<number :to="to" :format="format" :duration="duration" />
|
||||
<number :to="to" :format="format" :duration="animationDuration" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -10,10 +10,16 @@ export default {
|
|||
props: {
|
||||
to: { type: Number },
|
||||
format: { type: Function },
|
||||
noAnimation: { type: Boolean },
|
||||
},
|
||||
data() {
|
||||
return { duration: 0 };
|
||||
},
|
||||
computed: {
|
||||
animationDuration() {
|
||||
return this.noAnimation ? 0 : this.duration;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
to: function () {
|
||||
this.duration = DURATION;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,26 @@ Base.args = {
|
|||
activePhases: 2,
|
||||
};
|
||||
|
||||
export const WithoutSoc = Template.bind({});
|
||||
WithoutSoc.args = {
|
||||
id: 0,
|
||||
pvConfigured: true,
|
||||
chargePower: 2800,
|
||||
chargedEnergy: 7123,
|
||||
chargeDuration: 95 * 60,
|
||||
vehiclePresent: false,
|
||||
enabled: true,
|
||||
connected: true,
|
||||
mode: "pv",
|
||||
charging: true,
|
||||
vehicleSoC: 66,
|
||||
targetSoC: 90,
|
||||
chargeCurrent: 7,
|
||||
minCurrent: 6,
|
||||
maxCurrent: 16,
|
||||
activePhases: 2,
|
||||
};
|
||||
|
||||
export const Idle = Template.bind({});
|
||||
Idle.args = {
|
||||
id: 0,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
/>
|
||||
</div>
|
||||
<LabelAndValue
|
||||
v-show="socBasedCharging"
|
||||
:label="$t('main.loadpoint.charged')"
|
||||
:value="fmtKWh(chargedEnergy)"
|
||||
align="center"
|
||||
|
|
@ -92,6 +93,7 @@
|
|||
<Vehicle
|
||||
v-bind="vehicle"
|
||||
@target-soc-updated="setTargetSoC"
|
||||
@target-energy-updated="setTargetEnergy"
|
||||
@target-time-updated="setTargetTime"
|
||||
@target-time-removed="removeTargetTime"
|
||||
@change-vehicle="changeVehicle"
|
||||
|
|
@ -132,6 +134,7 @@ export default {
|
|||
title: String,
|
||||
mode: String,
|
||||
targetSoC: Number,
|
||||
targetEnergy: Number,
|
||||
remoteDisabled: Boolean,
|
||||
remoteDisabledSource: String,
|
||||
chargeDuration: Number,
|
||||
|
|
@ -147,6 +150,8 @@ export default {
|
|||
vehicleSoC: Number,
|
||||
vehicleTitle: String,
|
||||
vehicleTargetSoC: Number,
|
||||
vehicleCapacity: Number,
|
||||
vehicleFeatureOffline: Boolean,
|
||||
vehicles: Array,
|
||||
minSoC: Number,
|
||||
targetTime: String,
|
||||
|
|
@ -170,7 +175,6 @@ export default {
|
|||
maxCurrent: Number,
|
||||
phasesActive: Number,
|
||||
chargeCurrent: Number,
|
||||
vehicleCapacity: Number,
|
||||
connectedDuration: Number,
|
||||
chargeCurrents: Array,
|
||||
chargeConfigured: Boolean,
|
||||
|
|
@ -205,6 +209,9 @@ export default {
|
|||
showChargingIndicator: function () {
|
||||
return this.charging && this.chargePower > 0;
|
||||
},
|
||||
socBasedCharging: function () {
|
||||
return !this.vehicleFeatureOffline && this.vehiclePresent;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
phaseRemaining() {
|
||||
|
|
@ -250,6 +257,9 @@ export default {
|
|||
setTargetSoC: function (soc) {
|
||||
api.post(this.apiPath("targetsoc") + "/" + soc);
|
||||
},
|
||||
setTargetEnergy: function (kWh) {
|
||||
api.post(this.apiPath("targetenergy") + "/" + kWh);
|
||||
},
|
||||
setMaxCurrent: function (maxCurrent) {
|
||||
api.post(this.apiPath("maxcurrent") + "/" + maxCurrent);
|
||||
},
|
||||
|
|
|
|||
119
assets/js/components/TargetEnergySelect.vue
Normal file
119
assets/js/components/TargetEnergySelect.vue
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<LabelAndValue class="flex-grow-1" :label="$t('main.targetEnergy.label')" align="end">
|
||||
<h3 class="value m-0 d-block d-sm-flex align-items-baseline justify-content-end">
|
||||
<label class="position-relative">
|
||||
<select :value="targetEnergy" class="custom-select" @change="change">
|
||||
<option
|
||||
v-for="{ energy, text, disabled } in options"
|
||||
:key="energy"
|
||||
:value="energy"
|
||||
:disabled="disabled"
|
||||
>
|
||||
{{ text }}
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
class="text-decoration-underline"
|
||||
:class="{ 'text-gray fw-normal': !targetEnergy }"
|
||||
>
|
||||
<AnimatedNumber
|
||||
:to="targetEnergy"
|
||||
:format="formatKWh"
|
||||
:no-animation="!targetEnergy"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div v-if="estimatedTargetSoC" class="extraValue ms-0 ms-sm-1 text-nowrap">
|
||||
<AnimatedNumber :to="estimatedTargetSoC" :format="formatSoC" />
|
||||
</div>
|
||||
</h3>
|
||||
</LabelAndValue>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LabelAndValue from "./LabelAndValue.vue";
|
||||
import AnimatedNumber from "./AnimatedNumber.vue";
|
||||
import formatter from "../mixins/formatter";
|
||||
|
||||
export default {
|
||||
name: "TargetEnergySelect",
|
||||
components: { LabelAndValue, AnimatedNumber },
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
targetEnergy: Number,
|
||||
socPerKwh: Number,
|
||||
chargedEnergy: Number,
|
||||
vehicleCapacity: Number,
|
||||
},
|
||||
emits: ["target-energy-updated"],
|
||||
computed: {
|
||||
maxEnergy: function () {
|
||||
return this.vehicleCapacity || 100;
|
||||
},
|
||||
steps: function () {
|
||||
if (this.maxEnergy < 25) {
|
||||
return 1;
|
||||
}
|
||||
if (this.maxEnergy < 50) {
|
||||
return 2;
|
||||
}
|
||||
return 5;
|
||||
},
|
||||
options: function () {
|
||||
const result = [];
|
||||
for (let energy = 0; energy <= this.maxEnergy; energy += this.steps) {
|
||||
let text = this.formatKWh(energy);
|
||||
const disabled = energy < this.chargedEnergy / 1e3 && energy !== 0;
|
||||
const soc = this.estimatedSoC(energy);
|
||||
if (soc) {
|
||||
text += ` (${this.formatSoC(soc)})`;
|
||||
}
|
||||
result.push({ energy, text, disabled });
|
||||
}
|
||||
return result;
|
||||
},
|
||||
estimatedTargetSoC: function () {
|
||||
return this.estimatedSoC(this.targetEnergy);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
change: function (e) {
|
||||
return this.$emit("target-energy-updated", parseInt(e.target.value, 10));
|
||||
},
|
||||
estimatedSoC: function (kWh) {
|
||||
if (this.socPerKwh) {
|
||||
return Math.round(kWh * this.socPerKwh);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
formatKWh: function (value) {
|
||||
if (value === 0) {
|
||||
return this.$t("main.targetEnergy.noLimit");
|
||||
}
|
||||
return `${Math.round(value)} kWh`;
|
||||
},
|
||||
formatSoC: function (value) {
|
||||
return `+${Math.round(value)}%`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.value {
|
||||
font-size: 18px;
|
||||
}
|
||||
.extraValue {
|
||||
color: var(--evcc-gray);
|
||||
font-size: 14px;
|
||||
}
|
||||
.custom-select {
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -24,6 +24,7 @@ Base.args = {
|
|||
vehicleSoC: 42,
|
||||
vehicleRange: 231,
|
||||
targetSoC: 90,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ Connected.args = {
|
|||
charging: false,
|
||||
vehicleSoC: 66,
|
||||
targetSoC: 90,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
|
|
@ -48,6 +50,7 @@ ReadyToCharge.args = {
|
|||
charging: false,
|
||||
vehicleSoC: 66,
|
||||
targetSoC: 90,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
|
|
@ -60,6 +63,7 @@ Charging.args = {
|
|||
charging: true,
|
||||
vehicleSoC: 66,
|
||||
targetSoC: 90,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
|
|
@ -81,6 +85,7 @@ TargetChargePlanned.args = {
|
|||
targetTimeActive: false,
|
||||
targetSoC: 45,
|
||||
targetTime: hoursFromNow(14),
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
|
|
@ -96,6 +101,7 @@ TargetChargeActive.args = {
|
|||
targetTimeActive: true,
|
||||
targetSoC: 80,
|
||||
targetTime: hoursFromNow(2),
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
|
|
@ -109,6 +115,21 @@ MinCharge.args = {
|
|||
minSoC: 20,
|
||||
charging: true,
|
||||
targetSoC: 90,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
export const VehicleTargetSoc = Template.bind({});
|
||||
VehicleTargetSoc.args = {
|
||||
vehicleTitle: "Mein Auto",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
vehiclePresent: true,
|
||||
vehicleTargetSoC: 80,
|
||||
vehicleSoC: 66,
|
||||
charging: true,
|
||||
targetSoC: 90,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
|
|
@ -125,6 +146,7 @@ TimerPvEnable.args = {
|
|||
phaseRemainingInterpolated: 0,
|
||||
pvAction: "enable",
|
||||
pvRemainingInterpolated: 32,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
|
|
@ -141,16 +163,17 @@ TimerPvDisable.args = {
|
|||
phaseRemainingInterpolated: 0,
|
||||
pvAction: "disable",
|
||||
pvRemainingInterpolated: 155,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
export const UnknownVehicleConnected = Template.bind({});
|
||||
UnknownVehicleConnected.args = {
|
||||
vehicleTitle: "Mein Auto",
|
||||
enabled: false,
|
||||
connected: true,
|
||||
vehiclePresent: false,
|
||||
targetSoC: 90,
|
||||
socBasedCharging: false,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
|
|
@ -162,6 +185,7 @@ UnknownVehicleReadyToCharge.args = {
|
|||
vehiclePresent: false,
|
||||
charging: false,
|
||||
targetSoC: 100,
|
||||
socBasedCharging: false,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
|
|
@ -173,6 +197,37 @@ UnknownVehicleCharging.args = {
|
|||
vehiclePresent: false,
|
||||
charging: true,
|
||||
targetSoC: 90,
|
||||
socBasedCharging: false,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
export const OfflineVehicleCharging = Template.bind({});
|
||||
OfflineVehicleCharging.args = {
|
||||
vehicleTitle: "Polestar 2",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
vehiclePresent: true,
|
||||
vehicleFeatureOffline: true,
|
||||
vehicleCapacity: 72,
|
||||
chargedEnergy: 14123,
|
||||
charging: true,
|
||||
targetSoC: 90,
|
||||
socBasedCharging: false,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
export const OfflineVehicleTargetEnergy = Template.bind({});
|
||||
OfflineVehicleTargetEnergy.args = {
|
||||
vehicleTitle: "Polestar 2",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
vehiclePresent: true,
|
||||
vehicleFeatureOffline: true,
|
||||
vehicleCapacity: 72,
|
||||
chargedEnergy: 14123,
|
||||
targetEnergy: 30,
|
||||
charging: true,
|
||||
socBasedCharging: false,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,44 +12,65 @@
|
|||
@target-soc-updated="targetSocUpdated"
|
||||
@target-soc-drag="targetSocDrag"
|
||||
/>
|
||||
<div v-if="vehiclePresent">
|
||||
<div class="details d-flex flex-wrap justify-content-between">
|
||||
<LabelAndValue
|
||||
class="flex-grow-1"
|
||||
:label="$t('main.vehicle.vehicleSoC')"
|
||||
:value="vehicleSoC ? `${vehicleSoC}%` : '--'"
|
||||
:extraValue="vehicleRange ? `${vehicleRange} km` : null"
|
||||
align="start"
|
||||
/>
|
||||
<TargetCharge
|
||||
class="flex-grow-1 text-center target-charge"
|
||||
v-bind="targetCharge"
|
||||
:disabled="targetChargeDisabled"
|
||||
@target-time-updated="setTargetTime"
|
||||
@target-time-removed="removeTargetTime"
|
||||
/>
|
||||
<TargetSoCSelect
|
||||
class="flex-grow-1 text-end"
|
||||
:target-soc="displayTargetSoC"
|
||||
:range-per-soc="rangePerSoC"
|
||||
@target-soc-updated="targetSocUpdated"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="$hiddenFeatures" class="d-flex justify-content-start">
|
||||
<small>vor 5 Stunden</small>
|
||||
</div>
|
||||
|
||||
<div class="details d-flex flex-wrap justify-content-between">
|
||||
<LabelAndValue
|
||||
v-if="socBasedCharging"
|
||||
class="flex-grow-1"
|
||||
:label="$t('main.vehicle.vehicleSoC')"
|
||||
:value="vehicleSoC ? `${vehicleSoC}%` : '--'"
|
||||
:extraValue="vehicleRange ? `${vehicleRange} km` : null"
|
||||
align="start"
|
||||
/>
|
||||
<LabelAndValue
|
||||
v-else
|
||||
class="flex-grow-1"
|
||||
:label="$t('main.loadpoint.charged')"
|
||||
:value="fmtKWh(chargedEnergy)"
|
||||
:extraValue="chargedSoC"
|
||||
align="start"
|
||||
/>
|
||||
<TargetCharge
|
||||
v-if="socBasedCharging"
|
||||
class="flex-grow-1 text-center target-charge"
|
||||
v-bind="targetCharge"
|
||||
:disabled="targetChargeDisabled"
|
||||
@target-time-updated="setTargetTime"
|
||||
@target-time-removed="removeTargetTime"
|
||||
/>
|
||||
<TargetSoCSelect
|
||||
v-if="socBasedCharging"
|
||||
class="flex-grow-1 text-end"
|
||||
:target-soc="displayTargetSoC"
|
||||
:range-per-soc="rangePerSoC"
|
||||
@target-soc-updated="targetSocUpdated"
|
||||
/>
|
||||
<TargetEnergySelect
|
||||
v-else
|
||||
class="flex-grow-1 text-end"
|
||||
:target-energy="targetEnergy"
|
||||
:soc-per-kwh="socPerKwh"
|
||||
:charged-energy="chargedEnergy"
|
||||
:vehicle-capacity="vehicleCapacity"
|
||||
@target-energy-updated="targetEnergyUpdated"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="$hiddenFeatures" class="d-flex justify-content-start">
|
||||
<small>vor 5 Stunden</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import collector from "../mixins/collector";
|
||||
import formatter from "../mixins/formatter";
|
||||
import LabelAndValue from "./LabelAndValue.vue";
|
||||
import VehicleTitle from "./VehicleTitle.vue";
|
||||
import VehicleSoc from "./VehicleSoc.vue";
|
||||
import VehicleStatus from "./VehicleStatus.vue";
|
||||
import TargetCharge from "./TargetCharge.vue";
|
||||
import TargetSoCSelect from "./TargetSoCSelect.vue";
|
||||
import TargetEnergySelect from "./TargetEnergySelect.vue";
|
||||
|
||||
export default {
|
||||
name: "Vehicle",
|
||||
|
|
@ -60,8 +81,9 @@ export default {
|
|||
LabelAndValue,
|
||||
TargetCharge,
|
||||
TargetSoCSelect,
|
||||
TargetEnergySelect,
|
||||
},
|
||||
mixins: [collector],
|
||||
mixins: [collector, formatter],
|
||||
props: {
|
||||
id: [String, Number],
|
||||
connected: Boolean,
|
||||
|
|
@ -74,10 +96,14 @@ export default {
|
|||
vehicleDetectionActive: Boolean,
|
||||
vehicleRange: Number,
|
||||
vehicleTitle: String,
|
||||
vehicleCapacity: Number,
|
||||
socBasedCharging: Boolean,
|
||||
targetTimeActive: Boolean,
|
||||
targetTime: String,
|
||||
targetTimeProjectedStart: String,
|
||||
targetSoC: Number,
|
||||
targetEnergy: Number,
|
||||
chargedEnergy: Number,
|
||||
mode: String,
|
||||
phaseAction: String,
|
||||
phaseRemainingInterpolated: Number,
|
||||
|
|
@ -90,6 +116,7 @@ export default {
|
|||
"target-time-removed",
|
||||
"target-time-updated",
|
||||
"target-soc-updated",
|
||||
"target-energy-updated",
|
||||
"change-vehicle",
|
||||
"remove-vehicle",
|
||||
],
|
||||
|
|
@ -117,6 +144,16 @@ export default {
|
|||
}
|
||||
return null;
|
||||
},
|
||||
socPerKwh: function () {
|
||||
if (this.vehicleCapacity > 0) {
|
||||
return 100 / this.vehicleCapacity;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
chargedSoC: function () {
|
||||
const value = this.socPerKwh * (this.chargedEnergy / 1e3);
|
||||
return value > 1 ? `+${Math.round(value)}%` : null;
|
||||
},
|
||||
targetChargeDisabled: function () {
|
||||
return !this.connected || !["pv", "minpv"].includes(this.mode);
|
||||
},
|
||||
|
|
@ -134,6 +171,9 @@ export default {
|
|||
this.displayTargetSoC = targetSoC;
|
||||
this.$emit("target-soc-updated", targetSoC);
|
||||
},
|
||||
targetEnergyUpdated: function (targetEnergy) {
|
||||
this.$emit("target-energy-updated", targetEnergy);
|
||||
},
|
||||
setTargetTime: function (targetTime) {
|
||||
this.$emit("target-time-updated", targetTime);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="vehicle-soc">
|
||||
<div class="progress">
|
||||
<div
|
||||
v-if="connected || parked"
|
||||
v-if="connected"
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:class="{
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
</div>
|
||||
<div class="target">
|
||||
<input
|
||||
v-if="vehiclePresent && (connected || parked)"
|
||||
v-if="socBasedCharging && connected"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
|
|
@ -63,7 +63,9 @@ export default {
|
|||
charging: Boolean,
|
||||
minSoC: Number,
|
||||
targetSoC: Number,
|
||||
parked: Boolean,
|
||||
targetEnergy: Number,
|
||||
chargedEnergy: Number,
|
||||
socBasedCharging: Boolean,
|
||||
},
|
||||
emits: ["target-soc-drag", "target-soc-updated"],
|
||||
data: function () {
|
||||
|
|
@ -75,16 +77,23 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
vehicleSoCDisplayWidth: function () {
|
||||
if (this.vehiclePresent && this.vehicleSoC >= 0) {
|
||||
return this.vehicleSoC;
|
||||
if (this.socBasedCharging) {
|
||||
if (this.vehicleSoC >= 0) {
|
||||
return this.vehicleSoC;
|
||||
}
|
||||
return 100;
|
||||
} else {
|
||||
if (this.targetEnergy) {
|
||||
return (100 / this.targetEnergy) * (this.chargedEnergy / 1e3);
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
return 100;
|
||||
},
|
||||
vehicleTargetSoCActive: function () {
|
||||
return this.vehicleTargetSoC > 0 && this.vehicleTargetSoC > this.vehicleSoC;
|
||||
},
|
||||
targetSliderActive: function () {
|
||||
return !this.visibleTargetSoC || this.visibleTargetSoC <= this.vehicleTargetSoC;
|
||||
return !this.vehicleTargetSoC || this.visibleTargetSoC <= this.vehicleTargetSoC;
|
||||
},
|
||||
progressColor: function () {
|
||||
if (this.minSoCActive) {
|
||||
|
|
@ -96,16 +105,23 @@ export default {
|
|||
return this.minSoC > 0 && this.vehicleSoC < this.minSoC;
|
||||
},
|
||||
remainingSoCWidth: function () {
|
||||
if (this.vehicleSoCDisplayWidth === 100) {
|
||||
return null;
|
||||
}
|
||||
if (this.minSoCActive) {
|
||||
return this.minSoC - this.vehicleSoC;
|
||||
}
|
||||
let targetSoC = this.targetSliderActive ? this.visibleTargetSoC : this.vehicleTargetSoC;
|
||||
if (targetSoC > this.vehicleSoC) {
|
||||
return targetSoC - this.vehicleSoC;
|
||||
if (this.socBasedCharging) {
|
||||
if (this.vehicleSoCDisplayWidth === 100) {
|
||||
return null;
|
||||
}
|
||||
if (this.minSoCActive) {
|
||||
return this.minSoC - this.vehicleSoC;
|
||||
}
|
||||
let targetSoC = this.targetSliderActive
|
||||
? this.visibleTargetSoC
|
||||
: this.vehicleTargetSoC;
|
||||
if (targetSoC > this.vehicleSoC) {
|
||||
return targetSoC - this.vehicleSoC;
|
||||
}
|
||||
} else {
|
||||
return 100 - this.vehicleSoCDisplayWidth;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
visibleTargetSoC: function () {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@
|
|||
{{ name }}
|
||||
</span>
|
||||
</VehicleOptions>
|
||||
<span v-else class="flex-grow-1 text-truncate vehicle-name">
|
||||
{{ name }}
|
||||
</span>
|
||||
</h4>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -117,9 +120,11 @@ export default {
|
|||
margin-right: -0.75rem;
|
||||
}
|
||||
.vehicle-name {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--evcc-gray);
|
||||
}
|
||||
.options .vehicle-name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.spin {
|
||||
animation: rotation 1s infinite cubic-bezier(0.37, 0, 0.63, 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,6 +167,10 @@ export default {
|
|||
Bitte melde unerwartetes Verhalten in unseren
|
||||
`,
|
||||
},
|
||||
targetEnergy: {
|
||||
label: "Ladeziel",
|
||||
noLimit: "keins",
|
||||
},
|
||||
},
|
||||
startupError: {
|
||||
title: "Fehler beim Starten",
|
||||
|
|
|
|||
|
|
@ -166,6 +166,10 @@ export default {
|
|||
Please report unexpected behaviour in our
|
||||
`,
|
||||
},
|
||||
targetEnergy: {
|
||||
label: "Limit",
|
||||
noLimit: "none",
|
||||
},
|
||||
},
|
||||
startupError: {
|
||||
title: "Startup Error",
|
||||
|
|
|
|||
|
|
@ -192,6 +192,11 @@ vehicles:
|
|||
capacity: 80
|
||||
onidentify:
|
||||
targetsoc: 75
|
||||
- name: vehicle_3
|
||||
type: template
|
||||
template: offline
|
||||
title: grüner Honda e
|
||||
capacity: 26
|
||||
|
||||
site:
|
||||
title: Zuhause
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ type LoadPoint struct {
|
|||
Enable, Disable ThresholdConfig
|
||||
ResetOnDisconnect bool `mapstructure:"resetOnDisconnect"`
|
||||
onDisconnect api.ActionConfig
|
||||
targetEnergy int // Target charge energy for dumb vehicles
|
||||
|
||||
MinCurrent float64 // PV mode: start current Min+PV mode: min current
|
||||
MaxCurrent float64 // Max allowed current. Physically ensured by the charger
|
||||
|
|
@ -683,6 +684,18 @@ func (lp *LoadPoint) setStatus(status api.ChargeStatus) {
|
|||
lp.status = status
|
||||
}
|
||||
|
||||
// targetEnergyReached checks if target is configured and reached
|
||||
func (lp *LoadPoint) targetEnergyReached() bool {
|
||||
v, ok := lp.vehicle.(api.FeatureDescriber)
|
||||
if ok {
|
||||
ok = v.Has(api.Offline)
|
||||
}
|
||||
|
||||
return (v == nil || ok) &&
|
||||
lp.targetEnergy > 0 &&
|
||||
lp.chargedEnergy/1e3 >= float64(lp.targetEnergy)
|
||||
}
|
||||
|
||||
// targetSocReached checks if target is configured and reached.
|
||||
// If vehicle is not configured this will always return false
|
||||
func (lp *LoadPoint) targetSocReached() bool {
|
||||
|
|
@ -731,6 +744,17 @@ func (lp *LoadPoint) climateActive() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// disableUnlessClimater disables the charger unless climate is active
|
||||
func (lp *LoadPoint) disableUnlessClimater() error {
|
||||
var current float64 // zero disables
|
||||
if lp.climateActive() {
|
||||
lp.log.DEBUG.Println("climater active")
|
||||
current = lp.GetMinCurrent()
|
||||
}
|
||||
lp.socTimer.Reset() // once SoC is reached, the target charge request is removed
|
||||
return lp.setLimit(current, true)
|
||||
}
|
||||
|
||||
// remoteControlled returns true if remote control status is active
|
||||
func (lp *LoadPoint) remoteControlled(demand loadpoint.RemoteDemand) bool {
|
||||
lp.Lock()
|
||||
|
|
@ -864,6 +888,9 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) {
|
|||
lp.publish(vehicleOdometer, 0.0)
|
||||
}
|
||||
|
||||
// reset target energy
|
||||
lp.setTargetEnergy(0)
|
||||
|
||||
// re-publish vehicle settings
|
||||
lp.Unlock()
|
||||
lp.publish(phasesActive, lp.activePhases())
|
||||
|
|
@ -898,6 +925,17 @@ func (lp *LoadPoint) unpublishVehicle() {
|
|||
lp.publish(vehicleTargetSoC, 0.0)
|
||||
|
||||
lp.setRemainingDuration(-1)
|
||||
|
||||
lp.vehiclePublishFeature(api.Offline)
|
||||
}
|
||||
|
||||
// vehiclePublishFeature availability of vehicle features
|
||||
func (lp *LoadPoint) vehiclePublishFeature(f api.Feature) {
|
||||
v, ok := lp.vehicle.(api.FeatureDescriber)
|
||||
if ok {
|
||||
ok = v.Has(f)
|
||||
}
|
||||
lp.publish("vehicleFeature"+f.String(), ok)
|
||||
}
|
||||
|
||||
// vehicleUnidentified returns true if there are associated vehicles and detection is running.
|
||||
|
|
@ -1633,15 +1671,13 @@ func (lp *LoadPoint) Update(sitePower float64, cheap, batteryBuffered bool) {
|
|||
lp.log.DEBUG.Printf("switched phases: %dp", lp.ConfiguredPhases)
|
||||
}
|
||||
|
||||
case lp.targetEnergyReached():
|
||||
lp.log.DEBUG.Printf("targetEnergy reached: %.0fkWh > %dkWh", lp.chargedEnergy/1e3, lp.targetEnergy)
|
||||
err = lp.disableUnlessClimater()
|
||||
|
||||
case lp.targetSocReached():
|
||||
lp.log.DEBUG.Printf("targetSoC reached: %.1f > %d", lp.vehicleSoc, lp.SoC.target)
|
||||
var targetCurrent float64 // zero disables
|
||||
if lp.climateActive() {
|
||||
lp.log.DEBUG.Println("climater active")
|
||||
targetCurrent = lp.GetMinCurrent()
|
||||
}
|
||||
err = lp.setLimit(targetCurrent, true)
|
||||
lp.socTimer.Reset() // once SoC is reached, the target charge request is removed
|
||||
lp.log.DEBUG.Printf("targetSoC reached: %.1f%% > %d%%", lp.vehicleSoc, lp.SoC.target)
|
||||
err = lp.disableUnlessClimater()
|
||||
|
||||
// OCPP has priority over target charging
|
||||
case lp.remoteControlled(loadpoint.RemoteHardDisable):
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ type API interface {
|
|||
GetMode() api.ChargeMode
|
||||
// SetMode sets the charge mode
|
||||
SetMode(api.ChargeMode)
|
||||
// GetTargetEnergy returns the charge target energy
|
||||
GetTargetEnergy() int
|
||||
// SetTargetEnergy sets the charge target energy
|
||||
SetTargetEnergy(int)
|
||||
// GetTargetSoC returns the charge target soc
|
||||
GetTargetSoC() int
|
||||
// SetTargetSoC sets the charge target soc
|
||||
|
|
|
|||
|
|
@ -49,6 +49,38 @@ func (lp *LoadPoint) SetMode(mode api.ChargeMode) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetTargetSoC returns loadpoint charge target energy
|
||||
func (lp *LoadPoint) GetTargetEnergy() int {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
return lp.SoC.target
|
||||
}
|
||||
|
||||
// setTargetEnergy sets loadpoint charge target energy (no mutex)
|
||||
func (lp *LoadPoint) setTargetEnergy(energy int) {
|
||||
lp.targetEnergy = energy
|
||||
// test guard
|
||||
if lp.socTimer != nil {
|
||||
// TODO soctimer
|
||||
// lp.socTimer.Energy = energy
|
||||
}
|
||||
lp.publish("targetEnergy", energy)
|
||||
}
|
||||
|
||||
// SetTargetEnergy sets loadpoint charge target energy
|
||||
func (lp *LoadPoint) SetTargetEnergy(energy int) {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
|
||||
lp.log.DEBUG.Println("set target energy:", energy)
|
||||
|
||||
// apply immediately
|
||||
if lp.targetEnergy != energy {
|
||||
lp.setTargetEnergy(energy)
|
||||
lp.requestUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
// GetTargetSoC returns loadpoint charge target soc
|
||||
func (lp *LoadPoint) GetTargetSoC() int {
|
||||
lp.Lock()
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
|
|
@ -20,8 +20,8 @@
|
|||
<meta name="theme-color" content="#020318" />
|
||||
|
||||
<title>evcc</title>
|
||||
<script type="module" crossorigin src="./assets/index.a6e7d28f.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.682a2576.css">
|
||||
<script type="module" crossorigin src="./assets/index.019e1c40.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.5b6a2832.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
224
package-lock.json
generated
224
package-lock.json
generated
|
|
@ -78,30 +78,30 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.13.tgz",
|
||||
"integrity": "sha512-5yUzC5LqyTFp2HLmDoxGQelcdYgSpP9xsnMWBphAscOdFrHSAVbLNzWiy32sVNDqJRDiJK6klfDnAgu6PAGSHw==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.3.tgz",
|
||||
"integrity": "sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.13.tgz",
|
||||
"integrity": "sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz",
|
||||
"integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.1.0",
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/generator": "^7.18.13",
|
||||
"@babel/helper-compilation-targets": "^7.18.9",
|
||||
"@babel/helper-module-transforms": "^7.18.9",
|
||||
"@babel/helpers": "^7.18.9",
|
||||
"@babel/parser": "^7.18.13",
|
||||
"@babel/generator": "^7.19.3",
|
||||
"@babel/helper-compilation-targets": "^7.19.3",
|
||||
"@babel/helper-module-transforms": "^7.19.0",
|
||||
"@babel/helpers": "^7.19.0",
|
||||
"@babel/parser": "^7.19.3",
|
||||
"@babel/template": "^7.18.10",
|
||||
"@babel/traverse": "^7.18.13",
|
||||
"@babel/types": "^7.18.13",
|
||||
"@babel/traverse": "^7.19.3",
|
||||
"@babel/types": "^7.19.3",
|
||||
"convert-source-map": "^1.7.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
|
|
@ -117,12 +117,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.13.tgz",
|
||||
"integrity": "sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.3.tgz",
|
||||
"integrity": "sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.18.13",
|
||||
"@babel/types": "^7.19.3",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"jsesc": "^2.5.1"
|
||||
},
|
||||
|
|
@ -170,14 +170,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz",
|
||||
"integrity": "sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz",
|
||||
"integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.18.8",
|
||||
"@babel/compat-data": "^7.19.3",
|
||||
"@babel/helper-validator-option": "^7.18.6",
|
||||
"browserslist": "^4.20.2",
|
||||
"browserslist": "^4.21.3",
|
||||
"semver": "^6.3.0"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -263,13 +263,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-function-name": {
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz",
|
||||
"integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
|
||||
"integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.18.6",
|
||||
"@babel/types": "^7.18.9"
|
||||
"@babel/template": "^7.18.10",
|
||||
"@babel/types": "^7.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -312,9 +312,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz",
|
||||
"integrity": "sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz",
|
||||
"integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-environment-visitor": "^7.18.9",
|
||||
|
|
@ -322,9 +322,9 @@
|
|||
"@babel/helper-simple-access": "^7.18.6",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"@babel/template": "^7.18.6",
|
||||
"@babel/traverse": "^7.18.9",
|
||||
"@babel/types": "^7.18.9"
|
||||
"@babel/template": "^7.18.10",
|
||||
"@babel/traverse": "^7.19.0",
|
||||
"@babel/types": "^7.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -431,9 +431,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
|
||||
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
|
||||
"version": "7.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
|
||||
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -464,14 +464,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz",
|
||||
"integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz",
|
||||
"integrity": "sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.18.6",
|
||||
"@babel/traverse": "^7.18.9",
|
||||
"@babel/types": "^7.18.9"
|
||||
"@babel/template": "^7.18.10",
|
||||
"@babel/traverse": "^7.19.0",
|
||||
"@babel/types": "^7.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -492,9 +492,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
|
||||
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz",
|
||||
"integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==",
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
|
|
@ -1923,19 +1923,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.13.tgz",
|
||||
"integrity": "sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.3.tgz",
|
||||
"integrity": "sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/generator": "^7.18.13",
|
||||
"@babel/generator": "^7.19.3",
|
||||
"@babel/helper-environment-visitor": "^7.18.9",
|
||||
"@babel/helper-function-name": "^7.18.9",
|
||||
"@babel/helper-function-name": "^7.19.0",
|
||||
"@babel/helper-hoist-variables": "^7.18.6",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||
"@babel/parser": "^7.18.13",
|
||||
"@babel/types": "^7.18.13",
|
||||
"@babel/parser": "^7.19.3",
|
||||
"@babel/types": "^7.19.3",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
|
|
@ -1944,13 +1944,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
|
||||
"integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.3.tgz",
|
||||
"integrity": "sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.18.10",
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"@babel/helper-validator-identifier": "^7.19.1",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -24603,27 +24603,27 @@
|
|||
}
|
||||
},
|
||||
"@babel/compat-data": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.13.tgz",
|
||||
"integrity": "sha512-5yUzC5LqyTFp2HLmDoxGQelcdYgSpP9xsnMWBphAscOdFrHSAVbLNzWiy32sVNDqJRDiJK6klfDnAgu6PAGSHw==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.3.tgz",
|
||||
"integrity": "sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.13.tgz",
|
||||
"integrity": "sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz",
|
||||
"integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@ampproject/remapping": "^2.1.0",
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/generator": "^7.18.13",
|
||||
"@babel/helper-compilation-targets": "^7.18.9",
|
||||
"@babel/helper-module-transforms": "^7.18.9",
|
||||
"@babel/helpers": "^7.18.9",
|
||||
"@babel/parser": "^7.18.13",
|
||||
"@babel/generator": "^7.19.3",
|
||||
"@babel/helper-compilation-targets": "^7.19.3",
|
||||
"@babel/helper-module-transforms": "^7.19.0",
|
||||
"@babel/helpers": "^7.19.0",
|
||||
"@babel/parser": "^7.19.3",
|
||||
"@babel/template": "^7.18.10",
|
||||
"@babel/traverse": "^7.18.13",
|
||||
"@babel/types": "^7.18.13",
|
||||
"@babel/traverse": "^7.19.3",
|
||||
"@babel/types": "^7.19.3",
|
||||
"convert-source-map": "^1.7.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
|
|
@ -24632,12 +24632,12 @@
|
|||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.13.tgz",
|
||||
"integrity": "sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.3.tgz",
|
||||
"integrity": "sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.18.13",
|
||||
"@babel/types": "^7.19.3",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"jsesc": "^2.5.1"
|
||||
},
|
||||
|
|
@ -24675,14 +24675,14 @@
|
|||
}
|
||||
},
|
||||
"@babel/helper-compilation-targets": {
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz",
|
||||
"integrity": "sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz",
|
||||
"integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/compat-data": "^7.18.8",
|
||||
"@babel/compat-data": "^7.19.3",
|
||||
"@babel/helper-validator-option": "^7.18.6",
|
||||
"browserslist": "^4.20.2",
|
||||
"browserslist": "^4.21.3",
|
||||
"semver": "^6.3.0"
|
||||
}
|
||||
},
|
||||
|
|
@ -24741,13 +24741,13 @@
|
|||
}
|
||||
},
|
||||
"@babel/helper-function-name": {
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz",
|
||||
"integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
|
||||
"integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/template": "^7.18.6",
|
||||
"@babel/types": "^7.18.9"
|
||||
"@babel/template": "^7.18.10",
|
||||
"@babel/types": "^7.19.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-hoist-variables": {
|
||||
|
|
@ -24778,9 +24778,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/helper-module-transforms": {
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz",
|
||||
"integrity": "sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz",
|
||||
"integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-environment-visitor": "^7.18.9",
|
||||
|
|
@ -24788,9 +24788,9 @@
|
|||
"@babel/helper-simple-access": "^7.18.6",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"@babel/template": "^7.18.6",
|
||||
"@babel/traverse": "^7.18.9",
|
||||
"@babel/types": "^7.18.9"
|
||||
"@babel/template": "^7.18.10",
|
||||
"@babel/traverse": "^7.19.0",
|
||||
"@babel/types": "^7.19.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
|
|
@ -24867,9 +24867,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
|
||||
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
|
||||
"version": "7.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
|
||||
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-option": {
|
||||
|
|
@ -24891,14 +24891,14 @@
|
|||
}
|
||||
},
|
||||
"@babel/helpers": {
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz",
|
||||
"integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz",
|
||||
"integrity": "sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/template": "^7.18.6",
|
||||
"@babel/traverse": "^7.18.9",
|
||||
"@babel/types": "^7.18.9"
|
||||
"@babel/template": "^7.18.10",
|
||||
"@babel/traverse": "^7.19.0",
|
||||
"@babel/types": "^7.19.0"
|
||||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
|
|
@ -24913,9 +24913,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
|
||||
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg=="
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz",
|
||||
"integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ=="
|
||||
},
|
||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
|
||||
"version": "7.18.6",
|
||||
|
|
@ -25861,31 +25861,31 @@
|
|||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.13.tgz",
|
||||
"integrity": "sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.3.tgz",
|
||||
"integrity": "sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/generator": "^7.18.13",
|
||||
"@babel/generator": "^7.19.3",
|
||||
"@babel/helper-environment-visitor": "^7.18.9",
|
||||
"@babel/helper-function-name": "^7.18.9",
|
||||
"@babel/helper-function-name": "^7.19.0",
|
||||
"@babel/helper-hoist-variables": "^7.18.6",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||
"@babel/parser": "^7.18.13",
|
||||
"@babel/types": "^7.18.13",
|
||||
"@babel/parser": "^7.19.3",
|
||||
"@babel/types": "^7.19.3",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
|
||||
"integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
|
||||
"version": "7.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.3.tgz",
|
||||
"integrity": "sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-string-parser": "^7.18.10",
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"@babel/helper-validator-identifier": "^7.19.1",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
|
|||
|
||||
routes := map[string]route{
|
||||
"mode": {[]string{"POST", "OPTIONS"}, "/mode/{value:[a-z]+}", chargeModeHandler(lp)},
|
||||
"targetenergy": {[]string{"POST", "OPTIONS"}, "/targetenergy/{value:[0-9]+}", intHandler(pass(lp.SetTargetEnergy), lp.GetTargetEnergy)},
|
||||
"targetsoc": {[]string{"POST", "OPTIONS"}, "/targetsoc/{value:[0-9]+}", intHandler(pass(lp.SetTargetSoC), lp.GetTargetSoC)},
|
||||
"minsoc": {[]string{"POST", "OPTIONS"}, "/minsoc/{value:[0-9]+}", intHandler(pass(lp.SetMinSoC), lp.GetMinSoC)},
|
||||
"mincurrent": {[]string{"POST", "OPTIONS"}, "/mincurrent/{value:[0-9]+}", floatHandler(pass(lp.SetMinCurrent), lp.GetMinCurrent)},
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ params:
|
|||
valuetype: stringlist
|
||||
render: |
|
||||
type: custom
|
||||
features: ["offline"]
|
||||
{{- if ne .title "" }}
|
||||
title: {{ .title }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
package vehicle
|
||||
|
||||
import "github.com/evcc-io/evcc/api"
|
||||
import (
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type embed struct {
|
||||
Title_ string `mapstructure:"title"`
|
||||
Capacity_ int64 `mapstructure:"capacity"`
|
||||
Phases_ int `mapstructure:"phases"`
|
||||
Identifiers_ []string `mapstructure:"identifiers"`
|
||||
Features_ []api.Feature `mapstructure:"features"`
|
||||
OnIdentify api.ActionConfig `mapstructure:"onIdentify"`
|
||||
}
|
||||
|
||||
|
|
@ -34,3 +38,15 @@ func (v *embed) Identifiers() []string {
|
|||
func (v *embed) OnIdentified() api.ActionConfig {
|
||||
return v.OnIdentify
|
||||
}
|
||||
|
||||
var _ api.FeatureDescriber = (*embed)(nil)
|
||||
|
||||
// Features implements the api.Describer interface
|
||||
func (v *embed) Features() []api.Feature {
|
||||
return v.Features_
|
||||
}
|
||||
|
||||
// Features implements the api.Describer interface
|
||||
func (v *embed) Has(f api.Feature) bool {
|
||||
return slices.Contains(v.Features_, f)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue