Vehicles: move more properties to /vehicles (BC) (#11181)

This commit is contained in:
andig 2024-01-20 12:19:41 +01:00 • committed by GitHub
parent 02214bcdea
commit bc0343ab7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 182 additions and 162 deletions

View file

@ -2,88 +2,13 @@ package api
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
)
//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController
// ChargeMode is the charge operation mode. Valid values are off, now, minpv and pv
type ChargeMode string
// Charge modes
const (
ModeEmpty ChargeMode = ""
ModeOff ChargeMode = "off"
ModeNow ChargeMode = "now"
ModeMinPV ChargeMode = "minpv"
ModePV ChargeMode = "pv"
)
// String implements Stringer
func (c ChargeMode) String() string {
return string(c)
}
// ChargeStatus is the EV's charging status from A to F
type ChargeStatus string
// Charging states
const (
StatusNone ChargeStatus = ""
StatusA ChargeStatus = "A" // Fzg. angeschlossen: nein Laden aktiv: nein Ladestation betriebsbereit, Fahrzeug getrennt
StatusB ChargeStatus = "B" // Fzg. angeschlossen: ja Laden aktiv: nein Fahrzeug verbunden, Netzspannung liegt nicht an
StatusC ChargeStatus = "C" // Fzg. angeschlossen: ja Laden aktiv: ja Fahrzeug lädt, Netzspannung liegt an
StatusD ChargeStatus = "D" // Fzg. angeschlossen: ja Laden aktiv: ja Fahrzeug lädt mit externer Belüfungsanforderung (für Blei-Säure-Batterien)
StatusE ChargeStatus = "E" // Fzg. angeschlossen: ja Laden aktiv: nein Fehler Fahrzeug / Kabel (CP-Kurzschluss, 0V)
StatusF ChargeStatus = "F" // Fzg. angeschlossen: ja Laden aktiv: nein Fehler EVSE oder Abstecken simulieren (CP-Wake-up, -12V)
)
var StatusEasA = map[ChargeStatus]ChargeStatus{StatusE: StatusA}
// ChargeStatusString converts a string to ChargeStatus
func ChargeStatusString(status string) (ChargeStatus, error) {
s := strings.ToUpper(strings.Trim(status, "\x00 "))
if len(s) == 0 {
return StatusNone, fmt.Errorf("invalid status: %s", status)
}
switch s1 := s[:1]; s1 {
case "A", "B":
return ChargeStatus(s1), nil
case "C", "D":
if s == "C1" || s == "D1" {
return StatusB, nil
}
return StatusC, nil
case "E", "F":
return ChargeStatus(s1), fmt.Errorf("invalid status: %s", s)
default:
return StatusNone, fmt.Errorf("invalid status: %s", status)
}
}
// ChargeStatusStringWithMapping converts a string to ChargeStatus. In case of error, mapping is applied.
func ChargeStatusStringWithMapping(s string, m map[ChargeStatus]ChargeStatus) (ChargeStatus, error) {
status, err := ChargeStatusString(s)
if mappedStatus, ok := m[status]; ok && err != nil {
return mappedStatus, nil
}
return status, err
}
// String implements Stringer
func (c ChargeStatus) String() string {
return string(c)
}
// Meter provides total active power in W
type Meter interface {
CurrentPower() (float64, error)
@ -187,6 +112,7 @@ type Vehicle interface {
Battery
BatteryCapacity
IconDescriber
FeatureDescriber
Title() string
SetTitle(string)
Phases() int

79
api/chargemodestatus.go Normal file
View file

@ -0,0 +1,79 @@
package api
import (
"fmt"
"strings"
)
// ChargeMode is the charge operation mode. Valid values are off, now, minpv and pv
type ChargeMode string
// Charge modes
const (
ModeEmpty ChargeMode = ""
ModeOff ChargeMode = "off"
ModeNow ChargeMode = "now"
ModeMinPV ChargeMode = "minpv"
ModePV ChargeMode = "pv"
)
// String implements Stringer
func (c ChargeMode) String() string {
return string(c)
}
// ChargeStatus is the EV's charging status from A to F
type ChargeStatus string
// Charging states
const (
StatusNone ChargeStatus = ""
StatusA ChargeStatus = "A" // Fzg. angeschlossen: nein Laden aktiv: nein Ladestation betriebsbereit, Fahrzeug getrennt
StatusB ChargeStatus = "B" // Fzg. angeschlossen: ja Laden aktiv: nein Fahrzeug verbunden, Netzspannung liegt nicht an
StatusC ChargeStatus = "C" // Fzg. angeschlossen: ja Laden aktiv: ja Fahrzeug lädt, Netzspannung liegt an
StatusD ChargeStatus = "D" // Fzg. angeschlossen: ja Laden aktiv: ja Fahrzeug lädt mit externer Belüfungsanforderung (für Blei-Säure-Batterien)
StatusE ChargeStatus = "E" // Fzg. angeschlossen: ja Laden aktiv: nein Fehler Fahrzeug / Kabel (CP-Kurzschluss, 0V)
StatusF ChargeStatus = "F" // Fzg. angeschlossen: ja Laden aktiv: nein Fehler EVSE oder Abstecken simulieren (CP-Wake-up, -12V)
)
var StatusEasA = map[ChargeStatus]ChargeStatus{StatusE: StatusA}
// ChargeStatusString converts a string to ChargeStatus
func ChargeStatusString(status string) (ChargeStatus, error) {
s := strings.ToUpper(strings.Trim(status, "\x00 "))
if len(s) == 0 {
return StatusNone, fmt.Errorf("invalid status: %s", status)
}
switch s1 := s[:1]; s1 {
case "A", "B":
return ChargeStatus(s1), nil
case "C", "D":
if s == "C1" || s == "D1" {
return StatusB, nil
}
return StatusC, nil
case "E", "F":
return ChargeStatus(s1), fmt.Errorf("invalid status: %s", s)
default:
return StatusNone, fmt.Errorf("invalid status: %s", status)
}
}
// ChargeStatusStringWithMapping converts a string to ChargeStatus. In case of error, mapping is applied.
func ChargeStatusStringWithMapping(s string, m map[ChargeStatus]ChargeStatus) (ChargeStatus, error) {
status, err := ChargeStatusString(s)
if mappedStatus, ok := m[status]; ok && err != nil {
return mappedStatus, nil
}
return status, err
}
// String implements Stringer
func (c ChargeStatus) String() string {
return string(c)
}

View file

@ -361,6 +361,20 @@ func (mr *MockVehicleMockRecorder) Capacity() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capacity", reflect.TypeOf((*MockVehicle)(nil).Capacity))
}
// Features mocks base method.
func (m *MockVehicle) Features() []Feature {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Features")
ret0, _ := ret[0].([]Feature)
return ret0
}
// Features indicates an expected call of Features.
func (mr *MockVehicleMockRecorder) Features() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Features", reflect.TypeOf((*MockVehicle)(nil).Features))
}
// Icon mocks base method.
func (m *MockVehicle) Icon() string {
m.ctrl.T.Helper()

View file

@ -133,7 +133,7 @@ export default {
socBasedCharging: Boolean,
socPerKwh: Number,
vehicle: Object,
vehicleCapacity: Number,
capacity: Number,
vehicleSoc: Number,
vehicleTargetSoc: Number,
},
@ -208,7 +208,7 @@ export default {
}
return fmtEnergy(
this.planEnergy,
optionStep(this.vehicleCapacity || 100),
optionStep(this.capacity || 100),
this.fmtKWh,
this.$t("main.targetEnergy.noLimit")
);

View file

@ -6,7 +6,7 @@
:id="`${id}_0`"
class="mb-2"
v-bind="plans[0] || {}"
:vehicle-capacity="vehicleCapacity"
:capacity="capacity"
:range-per-soc="rangePerSoc"
:soc-per-kwh="socPerKwh"
:soc-based-planning="socBasedPlanning"
@ -58,7 +58,7 @@ export default {
smartCostType: String,
currency: String,
mode: String,
vehicleCapacity: Number,
capacity: Number,
vehicle: Object,
vehicleTargetSoc: Number,
},
@ -161,7 +161,7 @@ export default {
this.$emit("plan-updated", {
time: this.defaultDate(),
soc: 100,
energy: this.vehicleCapacity || 10,
energy: this.capacity || 10,
});
},
removePlan: function (index) {

View file

@ -147,7 +147,7 @@ export default {
time: String,
rangePerSoc: Number,
socPerKwh: Number,
vehicleCapacity: Number,
capacity: Number,
socBasedPlanning: Boolean,
},
emits: ["plan-updated", "plan-removed", "plan-preview"],
@ -173,7 +173,7 @@ export default {
energyOptions: function () {
const options = energyOptions(
0,
this.vehicleCapacity || 100,
this.capacity || 100,
this.socPerKwh,
this.fmtKWh,
"-"
@ -241,7 +241,7 @@ export default {
}
if (!this.selectedEnergy) {
this.selectedEnergy =
window.localStorage[LAST_ENERGY_GOAL_KEY] || this.vehicleCapacity || 10;
window.localStorage[LAST_ENERGY_GOAL_KEY] || this.capacity || 10;
}
let time = this.time;

View file

@ -38,7 +38,6 @@
:id="session.vehicle"
class="options"
:vehicles="vehicles"
:is-unknown="false"
connected
@change-vehicle="changeVehicle"
@remove-vehicle="removeVehicle"

View file

@ -47,21 +47,21 @@ export default {
limitEnergy: Number,
socPerKwh: Number,
chargedEnergy: Number,
vehicleCapacity: Number,
capacity: Number,
},
emits: ["limit-energy-updated"],
computed: {
options: function () {
return energyOptions(
this.chargedEnergy,
this.vehicleCapacity || 100,
this.capacity || 100,
this.socPerKwh,
this.fmtKWh,
this.$t("main.targetEnergy.noLimit")
);
},
step() {
return optionStep(this.vehicleCapacity || 100);
return optionStep(this.capacity || 100);
},
estimated: function () {
return estimatedSoc(this.limitEnergy, this.socPerKwh);

View file

@ -8,8 +8,8 @@ const state = reactive({
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
vehiclePresent: true,
vehicleTitle: "Mein Auto",
vehicleName: "meinauto",
enabled: true,
connected: true,
mode: "pv",
@ -29,14 +29,14 @@ const state = reactive({
<Loadpoint v-bind="state" />
</Variant>
<Variant title="without soc">
<Loadpoint v-bind="state" vehicleTitle="" :vehiclePresent="false" />
<Loadpoint v-bind="state" vehicleTitle="" vehicleName="" />
</Variant>
<Variant title="idle">
<Loadpoint
v-bind="state"
:enabled="false"
:connected="false"
:vehiclePresent="false"
vehicleName=""
mode="off"
:charging="false"
:chargeCurrent="0"

View file

@ -156,8 +156,6 @@ export default {
vehicleName: String,
vehicleIcon: String,
vehicleTargetSoc: Number,
vehicleCapacity: Number,
vehicleFeatureOffline: Boolean,
vehicles: Array,
planActive: Boolean,
planProjectedStart: String,
@ -247,13 +245,13 @@ export default {
return !!this.vehicleName;
},
vehicleHasSoc: function () {
return this.vehicleKnown && !this.vehicleFeatureOffline;
return this.vehicleKnown && !this.vehicle?.features?.includes("Offline");
},
socBasedCharging: function () {
return this.vehicleHasSoc || this.vehicleSoc > 0;
},
socBasedPlanning: function () {
return this.socBasedCharging && this.vehicleCapacity > 0;
return this.socBasedCharging && this.vehicle?.capacity > 0;
},
},
watch: {

View file

@ -9,7 +9,7 @@ function loadpoint(opts) {
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
vehiclePresent: true,
vehicleName: "tesla",
vehicleTitle: "Tesla Model 3",
enabled: true,
connected: true,

View file

@ -111,7 +111,8 @@ export default {
},
loadpointsCompact: function () {
return this.loadpoints.map((lp) => {
const icon = lp.chargerIcon || lp.vehicleIcon || "car";
const vehicleIcon = this.vehicles?.[lp.vehicleName]?.icon;
const icon = lp.chargerIcon || vehicleIcon || "car";
const charging = lp.charging;
const power = lp.chargePower || 0;
return { icon, charging, power };

View file

@ -3,14 +3,13 @@ import { reactive } from "vue";
import Vehicle from "./Vehicle.vue";
const state = reactive({
vehicleTitle: "Mein Auto",
vehicle: { title: "Mein Auto", icon: "car", capacity: 72, features: [] },
enabled: false,
connected: true,
vehiclePresent: true,
vehicleName: "meinauto",
vehicleSoc: 42.742,
vehicleRange: 231,
limitSoc: 90,
vehicleCapacity: 72,
chargedEnergy: 14123,
socBasedCharging: true,
id: 0,
@ -45,9 +44,9 @@ const hoursFromNow = function (hours) {
v-bind="state"
enabled
charging
:vehiclePresent="false"
vehicleName=""
:socBasedCharging="false"
:vehicleCapacity="null"
:vehicle="{ ...state.vehicle, capacity: null }"
mode="pv"
/>
</Variant>
@ -56,10 +55,13 @@ const hoursFromNow = function (hours) {
v-bind="state"
enabled
charging
vehicleTitle="Opel Corsa-e"
:socBasedCharging="false"
:vehicleCapacity="72"
vehicleFeatureOffline
:vehicle="{
...state.vehicle,
title: 'Opel Corsa-e',
capacity: 72,
features: ['Offline'],
}"
mode="pv"
/>
</Variant>
@ -68,11 +70,14 @@ const hoursFromNow = function (hours) {
v-bind="state"
enabled
charging
vehicleTitle="Opel Corsa-e"
:socBasedCharging="false"
:vehicleCapacity="72"
:vehicle="{
...state.vehicle,
title: 'Opel Corsa-e',
capacity: 72,
features: ['Offline'],
}"
:targetEnergy="30"
vehicleFeatureOffline
mode="pv"
/>
</Variant>

View file

@ -52,7 +52,7 @@
:limit-energy="limitEnergy"
:soc-per-kwh="socPerKwh"
:charged-energy="chargedEnergy"
:vehicle-capacity="vehicleCapacity"
:capacity="capacity"
@limit-energy-updated="limitEnergyUpdated"
/>
</div>
@ -117,16 +117,12 @@ export default {
tariffCo2: Number,
tariffGrid: Number,
vehicle: Object,
vehicleCapacity: Number,
vehicleDetectionActive: Boolean,
vehicleIcon: String,
vehicleName: String,
vehiclePresent: Boolean,
vehicleRange: Number,
vehicles: Array,
vehicleSoc: Number,
vehicleTargetSoc: Number,
vehicleTitle: String,
},
emits: ["limit-soc-updated", "limit-energy-updated", "change-vehicle", "remove-vehicle"],
data() {
@ -135,6 +131,15 @@ export default {
};
},
computed: {
title: function () {
return this.vehicle?.title || "";
},
capacity: function () {
return this.vehicle?.capacity || 0;
},
icon: function () {
return this.vehicle?.icon || "";
},
minSoc: function () {
return this.vehicle?.minSoc || 0;
},
@ -178,8 +183,8 @@ export default {
return null;
},
socPerKwh: function () {
if (this.vehicleCapacity > 0) {
return 100 / this.vehicleCapacity;
if (this.capacity > 0) {
return 100 / this.capacity;
}
return null;
},

View file

@ -19,7 +19,7 @@
{{ vehicle.title }}
</button>
</li>
<li v-if="!isUnknown">
<li>
<button type="button" class="dropdown-item" @click="removeVehicle()">
<span v-if="connected">{{ $t("main.vehicle.unknown") }}</span>
<span v-else>{{ $t("main.vehicle.none") }}</span>
@ -39,7 +39,6 @@ export default {
connected: Boolean,
id: [String, Number],
vehicles: Array,
isUnknown: Boolean,
},
emits: ["change-vehicle", "remove-vehicle"],
computed: {

View file

@ -81,7 +81,6 @@ export default {
name: "VehicleSoc",
props: {
connected: Boolean,
vehiclePresent: Boolean,
vehicleSoc: Number,
vehicleTargetSoc: Number,
enabled: Boolean,

View file

@ -2,15 +2,15 @@
<div class="d-flex justify-content-between mb-3 align-items-center" data-testid="vehicle-title">
<h4 class="d-flex align-items-center m-0 flex-grow-1 overflow-hidden">
<shopicon-regular-refresh
v-if="icon === 'refresh'"
v-if="iconType === 'refresh'"
ref="refresh"
data-bs-toggle="tooltip"
:title="$t('main.vehicle.detectionActive')"
class="me-2 flex-shrink-0 spin"
></shopicon-regular-refresh>
<VehicleIcon
v-else-if="icon === 'vehicle'"
:name="vehicleIcon"
v-else-if="iconType === 'vehicle'"
:name="icon"
class="me-2 flex-shrink-0"
/>
<shopicon-regular-cablecharge
@ -23,7 +23,6 @@
:id="id"
class="options"
:vehicles="otherVehicles"
:is-unknown="isUnknown"
@change-vehicle="changeVehicle"
@remove-vehicle="removeVehicle"
>
@ -54,14 +53,14 @@ export default {
connected: Boolean,
id: [String, Number],
vehicleDetectionActive: Boolean,
vehicleIcon: String,
icon: String,
vehicleName: String,
vehicles: { type: Array, default: () => [] },
vehicleTitle: String,
title: String,
},
emits: ["change-vehicle", "remove-vehicle"],
computed: {
icon() {
iconType() {
if (this.vehicleDetectionActive) {
return "refresh";
}
@ -71,8 +70,8 @@ export default {
return null;
},
name() {
if (this.vehicleTitle) {
return this.vehicleTitle;
if (this.title) {
return this.title;
}
if (this.connected) {
return this.$t("main.vehicle.unknown");
@ -93,7 +92,7 @@ export default {
},
},
watch: {
icon: function () {
iconType: function () {
this.tooltip();
},
},

View file

@ -60,15 +60,12 @@ const (
RemoteDisabledSource = "remoteDisabledSource" // remote disabled source
// vehicle
VehicleTitle = "vehicleTitle" // vehicle title
VehicleName = "vehicleName" // vehicle name
VehicleIdentity = "vehicleIdentity" // vehicle identity
VehicleCapacity = "vehicleCapacity" // vehicle battery capacity
VehicleDetectionActive = "vehicleDetectionActive" // vehicle detection active
VehicleIcon = "vehicleIcon" // vehicle icon for ui
VehicleOdometer = "vehicleOdometer" // vehicle odometer
VehicleRange = "vehicleRange" // vehicle range
VehicleSoc = "vehicleSoc" // vehicle soc
VehicleTargetSoc = "vehicleTargetSoc" // vehicle soc limit
VehicleTargetSoc = "vehicleTargetSoc" // vehicle api soc limit
VehicleClimaterActive = "vehicleClimaterActive" // vehicle climater active
)

View file

@ -591,9 +591,7 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
}
// vehicle
lp.publish(keys.VehicleIcon, "")
lp.publish(keys.VehicleName, "")
lp.publish(keys.VehicleCapacity, 0.0)
lp.publish(keys.VehicleOdometer, 0.0)
// assign and publish default vehicle

View file

@ -386,10 +386,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
vehicle := api.NewMockVehicle(ctrl)
// wrap vehicle with estimator
vehicle.EXPECT().Capacity().Return(float64(10)).AnyTimes()
vehicle.EXPECT().Phases().Return(0).AnyTimes()
vehicle.EXPECT().Title().Return("foo").AnyTimes() // TODO remove when settings always available for vehicle
vehicle.EXPECT().OnIdentified().Return(api.ActionConfig{}).AnyTimes()
expectVehiclePublish(vehicle)
socEstimator := soc.NewEstimator(util.NewLogger("foo"), charger, vehicle, false)

View file

@ -132,8 +132,6 @@ func (lp *Loadpoint) setActiveVehicle(v api.Vehicle) {
lp.socEstimator = soc.NewEstimator(lp.log, lp.charger, v, estimate)
lp.publish(keys.VehicleName, vehicle.Settings(lp.log, v).Name())
lp.publish(keys.VehicleIcon, v.Icon())
lp.publish(keys.VehicleCapacity, v.Capacity())
if mode, ok := v.OnIdentified().GetMode(); ok {
lp.SetMode(mode)
@ -146,8 +144,6 @@ func (lp *Loadpoint) setActiveVehicle(v api.Vehicle) {
lp.socEstimator = nil
lp.publish(keys.VehicleSoc, 0)
lp.publish(keys.VehicleName, "")
lp.publish(keys.VehicleIcon, "")
lp.publish(keys.VehicleCapacity, 0.0)
lp.publish(keys.VehicleOdometer, 0.0)
}
@ -197,8 +193,6 @@ func (lp *Loadpoint) unpublishVehicle() {
lp.setRemainingEnergy(0)
lp.setRemainingDuration(0)
lp.publishVehicleFeature(api.Offline)
}
// vehicleHasFeature checks availability of vehicle feature
@ -210,11 +204,6 @@ func (lp *Loadpoint) vehicleHasFeature(f api.Feature) bool {
return ok
}
// publishVehicleFeature availability of vehicle features
func (lp *Loadpoint) publishVehicleFeature(f api.Feature) {
lp.publish("vehicleFeature"+f.String(), lp.vehicleHasFeature(f))
}
// vehicleUnidentified returns true if there are associated vehicles and detection is running.
// It will also reset the api cache at regular intervals.
// Detection is stopped after maximum duration and the "guest vehicle" message dispatched.

View file

@ -15,6 +15,15 @@ import (
"go.uber.org/mock/gomock"
)
func expectVehiclePublish(vehicle *api.MockVehicle) {
vehicle.EXPECT().Title().Return("target").AnyTimes()
vehicle.EXPECT().Capacity().AnyTimes()
vehicle.EXPECT().Icon().AnyTimes()
vehicle.EXPECT().Features().AnyTimes()
vehicle.EXPECT().Phases().AnyTimes()
vehicle.EXPECT().OnIdentified().AnyTimes()
}
func TestPublishSocAndRange(t *testing.T) {
ctrl := gomock.NewController(t)
clck := clock.NewMock()
@ -24,10 +33,7 @@ func TestPublishSocAndRange(t *testing.T) {
charger.EXPECT().Enabled().Return(true, nil).AnyTimes()
vehicle := api.NewMockVehicle(ctrl)
vehicle.EXPECT().Title().Return("target").AnyTimes()
vehicle.EXPECT().Capacity().AnyTimes()
vehicle.EXPECT().Phases().AnyTimes()
vehicle.EXPECT().OnIdentified().AnyTimes()
expectVehiclePublish(vehicle)
log := util.NewLogger("foo")
lp := &Loadpoint{
@ -227,12 +233,9 @@ func TestReconnectVehicle(t *testing.T) {
*api.MockChargeState
}
vehicle := &vehicleT{api.NewMockVehicle(ctrl), api.NewMockChargeState(ctrl)}
vehicle.MockVehicle.EXPECT().Title().Return("vehicle").AnyTimes()
vehicle.MockVehicle.EXPECT().Icon().Return("").AnyTimes()
vehicle.MockVehicle.EXPECT().Capacity().AnyTimes()
vehicle.MockVehicle.EXPECT().Phases().AnyTimes()
vehicle.MockVehicle.EXPECT().OnIdentified().AnyTimes()
v := api.NewMockVehicle(ctrl)
vehicle := &vehicleT{v, api.NewMockChargeState(ctrl)}
expectVehiclePublish(v)
vehicle.MockVehicle.EXPECT().Identifiers().AnyTimes().Return(tc.vehicleId)
vehicle.MockVehicle.EXPECT().Soc().Return(0.0, nil).AnyTimes()

View file

@ -9,6 +9,7 @@ import (
"github.com/evcc-io/evcc/core/vehicle"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/config"
"github.com/samber/lo"
)
type planStruct struct {
@ -18,8 +19,11 @@ type planStruct struct {
type vehicleStruct struct {
Title string `json:"title"`
Icon string `json:"icon,omitempty"`
Capacity float64 `json:"capacity,omitempty"`
MinSoc int `json:"minSoc,omitempty"`
LimitSoc int `json:"limitSoc,omitempty"`
Features []string `json:"features,omitempty"`
Plans []planStruct `json:"plans,omitempty"`
}
@ -36,14 +40,19 @@ func (site *Site) publishVehicles() {
plans = append(plans, planStruct{Soc: soc, Time: time})
}
instance := v.Instance()
res[v.Name()] = vehicleStruct{
Title: v.Instance().Title(),
Title: instance.Title(),
Icon: instance.Icon(),
Capacity: instance.Capacity(),
MinSoc: v.GetMinSoc(),
LimitSoc: v.GetLimitSoc(),
Features: lo.Map(instance.Features(), func(f api.Feature, _ int) string { return f.String() }),
Plans: plans,
}
if lp := site.coordinator.Owner(v.Instance()); lp != nil {
if lp := site.coordinator.Owner(instance); lp != nil {
lp.PublishEffectiveValues()
}
}

View file

@ -79,7 +79,10 @@ func (h *Hub) apply(ev Event, tmpl string) (string, error) {
// add missing attributes
if name, ok := attr["vehicleName"].(string); ok {
if v, err := h.vehicles.ByName(name); err == nil {
attr["vehicleTitle"] = v.Instance().Title()
instance := v.Instance()
attr["vehicleTitle"] = instance.Title()
attr["vehicleIcon"] = instance.Icon()
attr["vehicleCapacity"] = instance.Capacity()
}
}

View file

@ -5,7 +5,6 @@ import (
)
// TODO align phases with OnIdentify
// TODO remove vehicle settings
type embed struct {
Title_ string `mapstructure:"title"`
Icon_ string `mapstructure:"icon"`

View file

@ -21,6 +21,7 @@ type Wrapper struct {
// New creates a new Vehicle
func New(name string, other map[string]interface{}, err error) api.Vehicle {
var cc struct {
// add manually since embed is not available here
Title string
Icon string
Phases int