Add API and UI to change a loadpoint vehicle manually (#3805)
This commit is contained in:
parent
f1b50d67b3
commit
1ae1fb34d9
22 changed files with 341 additions and 107 deletions
|
|
@ -278,9 +278,13 @@ small {
|
|||
.dropdown-item {
|
||||
color: var(--evcc-default-text);
|
||||
}
|
||||
.dropdown-item:active {
|
||||
background-color: var(--bs-primary);
|
||||
color: var(--bs-white);
|
||||
}
|
||||
|
||||
@media (--dark-mode) {
|
||||
.dropdown-menu {
|
||||
box-shadow: 0 0 8px var(--evcc-box);
|
||||
box-shadow: 0 0 8px var(--evcc-background);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@
|
|||
@target-soc-updated="setTargetSoC"
|
||||
@target-time-updated="setTargetTime"
|
||||
@target-time-removed="removeTargetTime"
|
||||
@change-vehicle="changeVehicle"
|
||||
@remove-vehicle="removeVehicle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -118,6 +120,7 @@ export default {
|
|||
vehicleSoC: Number,
|
||||
vehiclePresent: Boolean,
|
||||
vehicleRange: Number,
|
||||
vehicles: Array,
|
||||
minSoC: Number,
|
||||
targetTime: String,
|
||||
targetTimeActive: Boolean,
|
||||
|
|
@ -216,6 +219,12 @@ export default {
|
|||
removeTargetTime: function () {
|
||||
api.delete(this.apiPath("targetcharge"));
|
||||
},
|
||||
changeVehicle(index) {
|
||||
api.post(this.apiPath("vehicle") + `/${index}`);
|
||||
},
|
||||
removeVehicle() {
|
||||
api.delete(this.apiPath("vehicle"));
|
||||
},
|
||||
fmtkWUnit(value) {
|
||||
const inKw = value == 0 || value >= 1000;
|
||||
return this.fmtKw(value, inKw);
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ Multi.args = {
|
|||
vehicleTitle: "Mein drittes Auto",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
hasVehicle: true,
|
||||
charging: true,
|
||||
vehicleSoC: 22,
|
||||
vehicleRange: 34,
|
||||
|
|
|
|||
|
|
@ -195,3 +195,16 @@ DisconnectedKnownSoc.args = {
|
|||
targetSoC: 60,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
export const SwitchBetweenVehicles = Template.bind({});
|
||||
SwitchBetweenVehicles.args = {
|
||||
vehicleTitle: "Weißes Model 3",
|
||||
vehicles: ["Blauer e-Golf", "Weißes Model 3"],
|
||||
enabled: true,
|
||||
connected: true,
|
||||
vehiclePresent: true,
|
||||
vehicleSoC: 42,
|
||||
vehicleRange: 231,
|
||||
targetSoC: 90,
|
||||
id: 0,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<template>
|
||||
<div class="vehicle pt-4">
|
||||
<VehicleTitle v-bind="vehicleTitleProps" />
|
||||
<VehicleTitle
|
||||
v-bind="vehicleTitleProps"
|
||||
@change-vehicle="changeVehicle"
|
||||
@remove-vehicle="removeVehicle"
|
||||
/>
|
||||
<VehicleStatus v-if="!parked" v-bind="vehicleStatus" class="mb-2" />
|
||||
<VehicleSoc
|
||||
v-bind="vehicleSocProps"
|
||||
|
|
@ -78,8 +82,15 @@ export default {
|
|||
pvAction: String,
|
||||
pvRemainingInterpolated: Number,
|
||||
parked: Boolean,
|
||||
vehicles: Array,
|
||||
},
|
||||
emits: ["target-time-removed", "target-time-updated", "target-soc-updated"],
|
||||
emits: [
|
||||
"target-time-removed",
|
||||
"target-time-updated",
|
||||
"target-soc-updated",
|
||||
"change-vehicle",
|
||||
"remove-vehicle",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
displayTargetSoC: this.targetSoC,
|
||||
|
|
@ -127,6 +138,12 @@ export default {
|
|||
removeTargetTime: function () {
|
||||
this.$emit("target-time-removed");
|
||||
},
|
||||
changeVehicle(index) {
|
||||
this.$emit("change-vehicle", index);
|
||||
},
|
||||
removeVehicle() {
|
||||
this.$emit("remove-vehicle");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
53
assets/js/components/VehicleOptions.vue
Normal file
53
assets/js/components/VehicleOptions.vue
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-target="#navbarNavAltMarkup"
|
||||
aria-controls="navbarNavAltMarkup"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
class="btn btn-sm btn-outline-secondary position-relative border-0 menu-button"
|
||||
>
|
||||
<shopicon-filled-options size="s"></shopicon-filled-options>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<h6 class="dropdown-header">{{ $t("main.vehicle.changeVehicle") }}</h6>
|
||||
</li>
|
||||
<li v-for="vehicle in vehicles" :key="vehicle">
|
||||
<button type="button" class="dropdown-item" @click="changeVehicle(vehicle.id)">
|
||||
{{ vehicle.title }}
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="!isUnknown">
|
||||
<button type="button" class="dropdown-item" @click="removeVehicle()">
|
||||
{{ $t("main.vehicle.unknown") }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "@h2d2/shopicons/es/filled/options";
|
||||
|
||||
export default {
|
||||
name: "VehicleOptions",
|
||||
props: {
|
||||
vehicles: Array,
|
||||
isUnknown: Boolean,
|
||||
},
|
||||
emits: ["change-vehicle", "remove-vehicle"],
|
||||
methods: {
|
||||
changeVehicle(index) {
|
||||
this.$emit("change-vehicle", index);
|
||||
},
|
||||
removeVehicle() {
|
||||
this.$emit("remove-vehicle");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
@ -11,28 +11,36 @@
|
|||
></shopicon-regular-cablecharge>
|
||||
<span class="flex-grow-1 text-truncate"> {{ name }} </span>
|
||||
</h4>
|
||||
<button v-if="$hiddenFeatures" class="btn btn-link evcc-default-text p-0 flex-shrink-0">
|
||||
<shopicon-filled-options size="s"></shopicon-filled-options>
|
||||
</button>
|
||||
<VehicleOptions
|
||||
v-if="showOptions"
|
||||
class="options"
|
||||
:vehicles="otherVehicles"
|
||||
:is-unknown="isUnknown"
|
||||
@change-vehicle="changeVehicle"
|
||||
@remove-vehicle="removeVehicle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "@h2d2/shopicons/es/filled/options";
|
||||
import "@h2d2/shopicons/es/regular/car3";
|
||||
import "@h2d2/shopicons/es/regular/cablecharge";
|
||||
|
||||
import collector from "../mixins/collector";
|
||||
import VehicleOptions from "./VehicleOptions.vue";
|
||||
|
||||
export default {
|
||||
name: "VehicleTitle",
|
||||
components: { VehicleOptions },
|
||||
mixins: [collector],
|
||||
props: {
|
||||
vehiclePresent: Boolean,
|
||||
vehicleTitle: String,
|
||||
parked: Boolean,
|
||||
connected: Boolean,
|
||||
vehicles: { type: Array, default: () => [] },
|
||||
},
|
||||
emits: ["change-vehicle", "remove-vehicle"],
|
||||
computed: {
|
||||
carIcon() {
|
||||
return this.connected || this.parked;
|
||||
|
|
@ -46,6 +54,34 @@ export default {
|
|||
}
|
||||
return this.$t("main.vehicle.none");
|
||||
},
|
||||
isUnknown() {
|
||||
return this.connected && !this.vehiclePresent;
|
||||
},
|
||||
otherVehicles() {
|
||||
return this.vehicles
|
||||
.map((v, id) => ({
|
||||
id: id,
|
||||
title: v,
|
||||
}))
|
||||
.filter((v) => v.title !== this.vehicleTitle);
|
||||
},
|
||||
showOptions() {
|
||||
return !this.isUnknown || this.vehicles.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeVehicle(index) {
|
||||
this.$emit("change-vehicle", index);
|
||||
},
|
||||
removeVehicle() {
|
||||
this.$emit("remove-vehicle");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.options {
|
||||
margin-right: -0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ export default {
|
|||
targetSoC: "Ladeziel",
|
||||
none: "Kein Fahrzeug",
|
||||
unknown: "Gastfahrzeug",
|
||||
changeVehicle: "Fahrzeug ändern",
|
||||
},
|
||||
vehicleSoC: {
|
||||
disconnected: "getrennt",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ export default {
|
|||
targetSoC: "Limit",
|
||||
none: "No vehicle",
|
||||
unknown: "Guest vehicle",
|
||||
changeVehicle: "Change Vehicle",
|
||||
},
|
||||
vehicleSoC: {
|
||||
disconnected: "disconnected",
|
||||
|
|
|
|||
|
|
@ -193,6 +193,11 @@ vehicles:
|
|||
var lp = state.loadpoints[1]
|
||||
var range = (80 * lp.vehicleSoC) / 17;
|
||||
range
|
||||
status:
|
||||
source: js
|
||||
vm: shared
|
||||
script: |
|
||||
"B"
|
||||
capacity: 80
|
||||
|
||||
site:
|
||||
|
|
@ -211,13 +216,17 @@ loadpoints:
|
|||
target: 90
|
||||
meter: meter_charger_1
|
||||
vehicle: vehicle_1
|
||||
vehicles:
|
||||
- vehicle_2
|
||||
- title: Garage
|
||||
charger: charger_2
|
||||
soc:
|
||||
target: 75
|
||||
mode: "off"
|
||||
meter: meter_charger_2
|
||||
vehicle: vehicle_2
|
||||
vehicles:
|
||||
- vehicle_1
|
||||
- vehicle_2
|
||||
# - title: Garage rechts
|
||||
# charger: charger_3
|
||||
# mode: "off"
|
||||
|
|
|
|||
|
|
@ -2,22 +2,26 @@ package core
|
|||
|
||||
import (
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/loadpoint"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
type vehicleCoordinator struct {
|
||||
tracked map[api.Vehicle]interface{}
|
||||
tracked map[api.Vehicle]loadpoint.API
|
||||
}
|
||||
|
||||
var coordinator *vehicleCoordinator
|
||||
|
||||
func init() {
|
||||
coordinator = &vehicleCoordinator{
|
||||
tracked: make(map[api.Vehicle]interface{}),
|
||||
tracked: make(map[api.Vehicle]loadpoint.API),
|
||||
}
|
||||
}
|
||||
|
||||
func (lp *vehicleCoordinator) acquire(owner interface{}, vehicle api.Vehicle) {
|
||||
func (lp *vehicleCoordinator) acquire(owner loadpoint.API, vehicle api.Vehicle) {
|
||||
if o, ok := lp.tracked[vehicle]; ok && o != owner {
|
||||
o.SetVehicle(nil)
|
||||
}
|
||||
lp.tracked[vehicle] = owner
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +29,9 @@ func (lp *vehicleCoordinator) release(vehicle api.Vehicle) {
|
|||
delete(lp.tracked, vehicle)
|
||||
}
|
||||
|
||||
func (lp *vehicleCoordinator) availableVehicles(owner interface{}, vehicles []api.Vehicle) []api.Vehicle {
|
||||
// availableDetectibleVehicles is the list of vehicles that are currently not
|
||||
// associated to another loadpoint and have a status api that allows for detection
|
||||
func (lp *vehicleCoordinator) availableDetectibleVehicles(owner loadpoint.API, vehicles []api.Vehicle) []api.Vehicle {
|
||||
var res []api.Vehicle
|
||||
|
||||
for _, vv := range vehicles {
|
||||
|
|
@ -40,8 +46,8 @@ func (lp *vehicleCoordinator) availableVehicles(owner interface{}, vehicles []ap
|
|||
}
|
||||
|
||||
// find active vehicle by charge state
|
||||
func (lp *vehicleCoordinator) identifyVehicleByStatus(log *util.Logger, owner interface{}, vehicles []api.Vehicle) api.Vehicle {
|
||||
available := lp.availableVehicles(owner, vehicles)
|
||||
func (lp *vehicleCoordinator) identifyVehicleByStatus(log *util.Logger, owner loadpoint.API, vehicles []api.Vehicle) api.Vehicle {
|
||||
available := lp.availableDetectibleVehicles(owner, vehicles)
|
||||
|
||||
var res api.Vehicle
|
||||
for _, vehicle := range available {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/loadpoint"
|
||||
"github.com/evcc-io/evcc/mock"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/golang/mock/gomock"
|
||||
|
|
@ -38,15 +39,15 @@ func TestVehicleDetectByStatus(t *testing.T) {
|
|||
vehicles := []api.Vehicle{v1, v2}
|
||||
|
||||
lp := &LoadPoint{}
|
||||
c := &vehicleCoordinator{make(map[api.Vehicle]interface{})}
|
||||
c := &vehicleCoordinator{make(map[api.Vehicle]loadpoint.API)}
|
||||
|
||||
for _, tc := range tc {
|
||||
t.Logf("%+v", tc)
|
||||
|
||||
v1.MockChargeState.EXPECT().Status().Return(tc.v1, nil)
|
||||
v2.MockChargeState.EXPECT().Status().Return(tc.v2, nil)
|
||||
v1.MockVehicle.EXPECT().Title().Return("v1")
|
||||
v2.MockVehicle.EXPECT().Title().Return("v2")
|
||||
v1.MockVehicle.EXPECT().Title().Return("v1").AnyTimes()
|
||||
v2.MockVehicle.EXPECT().Title().Return("v2").AnyTimes()
|
||||
|
||||
res := c.identifyVehicleByStatus(log, lp, vehicles)
|
||||
if tc.res != res {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package core
|
|||
|
||||
import (
|
||||
"github.com/avast/retry-go/v3"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -37,3 +39,10 @@ func sitePower(log *util.Logger, maxGrid, grid, battery, residual float64) float
|
|||
|
||||
return grid + battery + residual
|
||||
}
|
||||
|
||||
// vehicleTitles returns a list of vehicle titles
|
||||
func vehicleTitles(vehicles []api.Vehicle) []string {
|
||||
return lo.Map(vehicles, func(v api.Vehicle, _ int) string {
|
||||
return v.Title()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/evcc/push"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
evbus "github.com/asaskevich/EventBus"
|
||||
|
|
@ -125,11 +126,12 @@ type LoadPoint struct {
|
|||
chargeTimer api.ChargeTimer
|
||||
chargeRater api.ChargeRater
|
||||
|
||||
chargeMeter api.Meter // Charger usage meter
|
||||
vehicle api.Vehicle // Currently active vehicle
|
||||
vehicles []api.Vehicle // Assigned vehicles
|
||||
socEstimator *soc.Estimator
|
||||
socTimer *soc.Timer
|
||||
chargeMeter api.Meter // Charger usage meter
|
||||
vehicle api.Vehicle // Currently active vehicle
|
||||
vehicles []api.Vehicle // Assigned vehicles
|
||||
defaultVehicle api.Vehicle // Default vehicle (disables detection)
|
||||
socEstimator *soc.Estimator
|
||||
socTimer *soc.Timer
|
||||
|
||||
// cached state
|
||||
status api.ChargeStatus // Charger status
|
||||
|
|
@ -199,13 +201,16 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
|
|||
lp.vehicles = append(lp.vehicles, vehicle)
|
||||
}
|
||||
|
||||
// single vehicle
|
||||
// default vehicle
|
||||
if lp.VehicleRef != "" {
|
||||
if len(lp.vehicles) > 0 {
|
||||
return nil, errors.New("cannot have vehicle and vehicles both")
|
||||
lp.defaultVehicle = cp.Vehicle(lp.VehicleRef)
|
||||
|
||||
// append default vehicle if not contained in list
|
||||
if len(lo.Filter(lp.vehicles, func(v api.Vehicle, _ int) bool {
|
||||
return v == lp.defaultVehicle
|
||||
})) == 0 {
|
||||
lp.vehicles = append(lp.vehicles, lp.defaultVehicle)
|
||||
}
|
||||
vehicle := cp.Vehicle(lp.VehicleRef)
|
||||
lp.vehicles = append(lp.vehicles, vehicle)
|
||||
}
|
||||
|
||||
// verify vehicle detection
|
||||
|
|
@ -405,8 +410,10 @@ func (lp *LoadPoint) evVehicleConnectHandler() {
|
|||
lp.log.DEBUG.Println("vehicle api refresh")
|
||||
provider.ResetCached()
|
||||
|
||||
// start detection if we have multiple vehicles
|
||||
if len(lp.vehicles) > 1 {
|
||||
// start detection if we have associated vehicles
|
||||
if lp.vehicle == lp.defaultVehicle {
|
||||
lp.setActiveVehicle(lp.defaultVehicle)
|
||||
} else if len(lp.vehicles) > 0 {
|
||||
lp.startVehicleDetection()
|
||||
}
|
||||
|
||||
|
|
@ -429,17 +436,10 @@ func (lp *LoadPoint) evVehicleDisconnectHandler() {
|
|||
|
||||
lp.pushEvent(evVehicleDisconnect)
|
||||
|
||||
// remove active vehicle if we have multiple vehicles
|
||||
if len(lp.vehicles) > 1 {
|
||||
// remove active vehicle if not default
|
||||
if lp.vehicle != lp.defaultVehicle {
|
||||
lp.setActiveVehicle(nil)
|
||||
}
|
||||
|
||||
// keep single vehicle to allow poll mode: always
|
||||
if len(lp.vehicles) == 1 {
|
||||
// but reset values if poll mode is not always (i.e. connected or charging)
|
||||
if lp.SoC.Poll.Mode != pollAlways {
|
||||
lp.unpublishVehicle()
|
||||
}
|
||||
lp.unpublishVehicle()
|
||||
}
|
||||
|
||||
// set default mode on disconnect
|
||||
|
|
@ -530,7 +530,7 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
|
|||
lp.publish("maxCurrent", lp.MaxCurrent)
|
||||
lp.publish("phases", lp.phases)
|
||||
lp.publish("activePhases", lp.activePhases())
|
||||
lp.publish("hasVehicle", len(lp.vehicles) > 0)
|
||||
lp.publishVehicles()
|
||||
|
||||
lp.Lock()
|
||||
lp.publish("mode", lp.Mode)
|
||||
|
|
@ -538,13 +538,11 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
|
|||
lp.publish("minSoC", lp.SoC.Min)
|
||||
lp.Unlock()
|
||||
|
||||
// always treat single vehicle as attached to allow poll mode: always
|
||||
if len(lp.vehicles) == 1 {
|
||||
lp.setActiveVehicle(lp.vehicles[0])
|
||||
}
|
||||
|
||||
// start detection if we have multiple vehicles
|
||||
if len(lp.vehicles) > 1 {
|
||||
// activate default vehicle (allows poll mode: always)
|
||||
if lp.defaultVehicle != nil {
|
||||
lp.setActiveVehicle(lp.defaultVehicle)
|
||||
} else if len(lp.vehicles) > 0 {
|
||||
// start detection if we have associated vehicles
|
||||
lp.startVehicleDetection()
|
||||
}
|
||||
|
||||
|
|
@ -871,9 +869,17 @@ func (lp *LoadPoint) startVehicleDetection() {
|
|||
lp.vehicleConnectedTicker = lp.clock.Ticker(vehicleDetectInterval)
|
||||
}
|
||||
|
||||
// vehicleUnidentified checks if loadpoint has multiple vehicles associated and starts discovery period
|
||||
// stopVehicleDetection expires the connection timer and ticker
|
||||
func (lp *LoadPoint) stopVehicleDetection() {
|
||||
lp.vehicleConnected = time.Time{}
|
||||
if lp.vehicleConnectedTicker != nil {
|
||||
lp.vehicleConnectedTicker.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// vehicleUnidentified checks if there are associated vehicles and starts discovery period
|
||||
func (lp *LoadPoint) vehicleUnidentified() bool {
|
||||
res := len(lp.vehicles) > 1 && lp.vehicle == nil &&
|
||||
res := len(lp.vehicles) > 0 && lp.vehicle == nil &&
|
||||
lp.clock.Since(lp.vehicleConnected) < vehicleDetectDuration
|
||||
|
||||
// request vehicle api refresh while waiting to identify
|
||||
|
|
@ -891,7 +897,7 @@ func (lp *LoadPoint) vehicleUnidentified() bool {
|
|||
|
||||
// identifyVehicleByStatus validates if the active vehicle is still connected to the loadpoint
|
||||
func (lp *LoadPoint) identifyVehicleByStatus() {
|
||||
if len(lp.vehicles) <= 1 {
|
||||
if len(lp.vehicles) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1127,6 +1133,11 @@ func (lp *LoadPoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6
|
|||
return false
|
||||
}
|
||||
|
||||
// publishVehicles publishes a slice of vehicle titles
|
||||
func (lp *LoadPoint) publishVehicles() {
|
||||
lp.publish("vehicles", vehicleTitles(lp.vehicles))
|
||||
}
|
||||
|
||||
// TODO move up to timer functions
|
||||
func (lp *LoadPoint) publishTimer(name string, delay time.Duration, action string) {
|
||||
timer := lp.pvTimer
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@ type API interface {
|
|||
|
||||
// SetTargetCharge sets the charge targetSoC
|
||||
SetTargetCharge(time.Time, int)
|
||||
// SetVehicle sets the active vehicle
|
||||
SetVehicle(vehicle api.Vehicle)
|
||||
// RemoteControl sets remote status demand
|
||||
RemoteControl(string, RemoteDemand)
|
||||
|
||||
|
|
@ -80,4 +78,13 @@ type API interface {
|
|||
GetRemainingDuration() time.Duration
|
||||
// GetRemainingEnergy is the remaining charge energy in Wh
|
||||
GetRemainingEnergy() float64
|
||||
|
||||
//
|
||||
// vehicles
|
||||
//
|
||||
|
||||
// GetVehicle is the list of vehicles
|
||||
GetVehicles() []api.Vehicle
|
||||
// SetVehicle sets the active vehicle
|
||||
SetVehicle(vehicle api.Vehicle)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,14 +143,6 @@ func (lp *LoadPoint) SetTargetCharge(finishAt time.Time, soc int) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetVehicle sets the active vehicle
|
||||
func (lp *LoadPoint) SetVehicle(vehicle api.Vehicle) {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
|
||||
lp.setActiveVehicle(vehicle)
|
||||
}
|
||||
|
||||
// RemoteControl sets remote status demand
|
||||
func (lp *LoadPoint) RemoteControl(source string, demand loadpoint.RemoteDemand) {
|
||||
lp.Lock()
|
||||
|
|
@ -234,9 +226,6 @@ func (lp *LoadPoint) GetMaxPower() float64 {
|
|||
|
||||
// setRemainingDuration sets the estimated remaining charging duration
|
||||
func (lp *LoadPoint) setRemainingDuration(chargeRemainingDuration time.Duration) {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
|
||||
if lp.chargeRemainingDuration != chargeRemainingDuration {
|
||||
lp.chargeRemainingDuration = chargeRemainingDuration
|
||||
lp.publish("chargeRemainingDuration", chargeRemainingDuration)
|
||||
|
|
@ -267,3 +256,22 @@ func (lp *LoadPoint) GetRemainingEnergy() float64 {
|
|||
defer lp.Unlock()
|
||||
return lp.chargeRemainingEnergy
|
||||
}
|
||||
|
||||
// GetVehicles is the list of vehicles
|
||||
func (lp *LoadPoint) GetVehicles() []api.Vehicle {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
return lp.vehicles
|
||||
}
|
||||
|
||||
// SetVehicle sets the active vehicle
|
||||
func (lp *LoadPoint) SetVehicle(vehicle api.Vehicle) {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
|
||||
// set desired vehicle
|
||||
lp.setActiveVehicle(vehicle)
|
||||
|
||||
// disable auto-detect
|
||||
lp.stopVehicleDetection()
|
||||
}
|
||||
|
|
|
|||
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.0f8a9c87.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.955a5e82.css">
|
||||
<script type="module" crossorigin src="./assets/index.f9e9d211.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.36f32821.css">
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ func NewHTTPd(addr string, site site.API, hub *SocketHub, cache *util.Cache) *HT
|
|||
"phases": {[]string{"POST", "OPTIONS"}, "/phases/{value:[0-9]+}", phasesHandler(lp)},
|
||||
"targetcharge": {[]string{"POST", "OPTIONS"}, "/targetcharge/{soc:[0-9]+}/{time:[0-9TZ:.-]+}", targetChargeHandler(lp)},
|
||||
"targetcharge2": {[]string{"DELETE", "OPTIONS"}, "/targetcharge", targetChargeRemoveHandler(lp)},
|
||||
"vehicle": {[]string{"DELETE", "OPTIONS"}, "/vehicle", vehicleRemoveHandler(lp)},
|
||||
"vehicle": {[]string{"POST", "OPTIONS"}, "/vehicle/{vehicle:[0-9]+}", vehicleHandler(lp)},
|
||||
"vehicle2": {[]string{"DELETE", "OPTIONS"}, "/vehicle", vehicleRemoveHandler(lp)},
|
||||
"remotedemand": {[]string{"POST", "OPTIONS"}, "/remotedemand/{demand:[a-z]+}/{source::[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,9 +112,9 @@ func intHandler(set func(int) error, get func() int) http.HandlerFunc {
|
|||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
val, err := strconv.ParseInt(vars["value"], 10, 32)
|
||||
val, err := strconv.Atoi(vars["value"])
|
||||
if err == nil {
|
||||
err = set(int(val))
|
||||
err = set(val)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -159,9 +159,9 @@ func phasesHandler(lp loadpoint.API) http.HandlerFunc {
|
|||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
phases, err := strconv.ParseInt(vars["value"], 10, 32)
|
||||
phases, err := strconv.Atoi(vars["value"])
|
||||
if err == nil {
|
||||
err = lp.SetPhases(int(phases))
|
||||
err = lp.SetPhases(phases)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -205,7 +205,7 @@ func targetChargeHandler(loadpoint targetCharger) http.HandlerFunc {
|
|||
vars := mux.Vars(r)
|
||||
|
||||
socS, ok := vars["soc"]
|
||||
socV, err := strconv.ParseInt(socS, 10, 32)
|
||||
socV, err := strconv.Atoi(socS)
|
||||
|
||||
if !ok || err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
|
@ -220,10 +220,10 @@ func targetChargeHandler(loadpoint targetCharger) http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
loadpoint.SetTargetCharge(timeV, int(socV))
|
||||
loadpoint.SetTargetCharge(timeV, socV)
|
||||
|
||||
res := struct {
|
||||
SoC int64 `json:"soc"`
|
||||
SoC int `json:"soc"`
|
||||
Time time.Time `json:"time"`
|
||||
}{
|
||||
SoC: socV,
|
||||
|
|
@ -243,6 +243,32 @@ func targetChargeRemoveHandler(loadpoint loadpoint.API) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// vehicleHandler sets active vehicle
|
||||
func vehicleHandler(loadpoint loadpoint.API) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
valS, ok := vars["vehicle"]
|
||||
val, err := strconv.Atoi(valS)
|
||||
|
||||
vehicles := loadpoint.GetVehicles()
|
||||
if !ok || val >= len(vehicles) || err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
loadpoint.SetVehicle(vehicles[val])
|
||||
|
||||
res := struct {
|
||||
Vehicle string `json:"vehicle"`
|
||||
}{
|
||||
Vehicle: vehicles[val].Title(),
|
||||
}
|
||||
|
||||
jsonResult(w, res)
|
||||
}
|
||||
}
|
||||
|
||||
// vehicleRemoveHandler removes vehicle
|
||||
func vehicleRemoveHandler(loadpoint loadpoint.API) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package server
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
|
|
@ -62,36 +63,58 @@ func (m *MQTT) publish(topic string, retained bool, payload interface{}) {
|
|||
payload = total
|
||||
}
|
||||
|
||||
if slice, ok := payload.([]string); ok && strings.HasSuffix(topic, "vehicles") {
|
||||
payload = len(slice)
|
||||
|
||||
// unpublish
|
||||
for i := len(slice); i < 10; i++ {
|
||||
slice = append(slice, "")
|
||||
}
|
||||
|
||||
// publish vehicles
|
||||
for i, v := range slice {
|
||||
m.publishSingleValue(fmt.Sprintf("%s/%d", topic, i), retained, v)
|
||||
}
|
||||
}
|
||||
|
||||
m.publishSingleValue(topic, retained, payload)
|
||||
}
|
||||
|
||||
func (m *MQTT) listenSetters(topic string, apiHandler loadpoint.API) {
|
||||
func (m *MQTT) listenSetters(topic string, lp loadpoint.API) {
|
||||
m.Handler.ListenSetter(topic+"/mode/set", func(payload string) {
|
||||
apiHandler.SetMode(api.ChargeMode(payload))
|
||||
lp.SetMode(api.ChargeMode(payload))
|
||||
})
|
||||
m.Handler.ListenSetter(topic+"/minSoC/set", func(payload string) {
|
||||
if soc, err := strconv.Atoi(payload); err == nil {
|
||||
apiHandler.SetMinSoC(soc)
|
||||
lp.SetMinSoC(soc)
|
||||
}
|
||||
})
|
||||
m.Handler.ListenSetter(topic+"/targetSoC/set", func(payload string) {
|
||||
if soc, err := strconv.Atoi(payload); err == nil {
|
||||
apiHandler.SetTargetSoC(soc)
|
||||
lp.SetTargetSoC(soc)
|
||||
}
|
||||
})
|
||||
m.Handler.ListenSetter(topic+"/minCurrent/set", func(payload string) {
|
||||
if current, err := strconv.ParseFloat(payload, 64); err == nil {
|
||||
apiHandler.SetMinCurrent(current)
|
||||
lp.SetMinCurrent(current)
|
||||
}
|
||||
})
|
||||
m.Handler.ListenSetter(topic+"/maxCurrent/set", func(payload string) {
|
||||
if current, err := strconv.ParseFloat(payload, 64); err == nil {
|
||||
apiHandler.SetMaxCurrent(current)
|
||||
lp.SetMaxCurrent(current)
|
||||
}
|
||||
})
|
||||
m.Handler.ListenSetter(topic+"/phases/set", func(payload string) {
|
||||
if phases, err := strconv.Atoi(payload); err == nil {
|
||||
_ = apiHandler.SetPhases(phases)
|
||||
_ = lp.SetPhases(phases)
|
||||
}
|
||||
})
|
||||
m.Handler.ListenSetter(topic+"/vehicle/set", func(payload string) {
|
||||
if vehicle, err := strconv.Atoi(payload); err == nil {
|
||||
vehicles := lp.GetVehicles()
|
||||
if vehicle < len(vehicles) {
|
||||
lp.SetVehicle(vehicles[vehicle])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue