Add API and UI to change a loadpoint vehicle manually (#3805)

This commit is contained in:
andig 2022-07-13 11:18:37 +02:00 • committed by GitHub
parent f1b50d67b3
commit 1ae1fb34d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 341 additions and 107 deletions

View file

@ -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);
}
}

View file

@ -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);

View file

@ -92,7 +92,6 @@ Multi.args = {
vehicleTitle: "Mein drittes Auto",
enabled: true,
connected: true,
hasVehicle: true,
charging: true,
vehicleSoC: 22,
vehicleRange: 34,

View file

@ -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,
};

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -86,6 +86,7 @@ export default {
targetSoC: "Ladeziel",
none: "Kein Fahrzeug",
unknown: "Gastfahrzeug",
changeVehicle: "Fahrzeug ändern",
},
vehicleSoC: {
disconnected: "getrennt",

View file

@ -86,6 +86,7 @@ export default {
targetSoC: "Limit",
none: "No vehicle",
unknown: "Guest vehicle",
changeVehicle: "Change Vehicle",
},
vehicleSoC: {
disconnected: "disconnected",

View file

@ -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"

View file

@ -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 {

View file

@ -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 {

View file

@ -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()
})
}

View file

@ -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

View file

@ -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)
}

View file

@ -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
View file

@ -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>

View file

@ -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)},
}

View file

@ -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) {

View file

@ -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])
}
}
})
}