Loadpoint: add welcomecharge feature (#2) (#14873)

This commit is contained in:
andig 2024-07-21 12:52:11 +02:00 • committed by GitHub
parent fbfe63fce5
commit dbd4c4fd92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 185 additions and 114 deletions

View file

@ -10,4 +10,5 @@ const (
IntegratedDevice
Heating
Retryable
WelcomeCharge
)

View file

@ -7,11 +7,11 @@ import (
"strings"
)
const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryable"
const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryableWelcomeCharge"
var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52}
var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52, 65}
const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryable"
const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryablewelcomecharge"
func (i Feature) String() string {
i -= 1
@ -30,9 +30,10 @@ func _FeatureNoOp() {
_ = x[IntegratedDevice-(3)]
_ = x[Heating-(4)]
_ = x[Retryable-(5)]
_ = x[WelcomeCharge-(6)]
}
var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable}
var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable, WelcomeCharge}
var _FeatureNameToValueMap = map[string]Feature{
_FeatureName[0:7]: Offline,
@ -45,6 +46,8 @@ var _FeatureNameToValueMap = map[string]Feature{
_FeatureLowerName[36:43]: Heating,
_FeatureName[43:52]: Retryable,
_FeatureLowerName[43:52]: Retryable,
_FeatureName[52:65]: WelcomeCharge,
_FeatureLowerName[52:65]: WelcomeCharge,
}
var _FeatureNames = []string{
@ -53,6 +56,7 @@ var _FeatureNames = []string{
_FeatureName[20:36],
_FeatureName[36:43],
_FeatureName[43:52],
_FeatureName[52:65],
}
// FeatureString retrieves an enum value from the enum constants string name.

View file

@ -170,6 +170,7 @@ export default {
// details
vehicleClimaterActive: Boolean,
vehicleWelcomeActive: Boolean,
chargePower: Number,
chargedEnergy: Number,
chargeRemainingDuration: Number,

View file

@ -0,0 +1,17 @@
<template>
<svg :style="svgStyle" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M11.875 20q.1 0 .2-.05t.15-.1l8.2-8.2q.3-.3.438-.675t.137-.75q0-.4-.137-.763t-.438-.637l-4.25-4.25q-.275-.3-.638-.437T14.776 4q-.375 0-.75.138t-.675.437l-.275.275l1.85 1.875q.375.35.55.8t.175.95q0 1.05-.712 1.763t-1.763.712q-.5 0-.962-.175t-.813-.525L9.525 8.4L5.15 12.775q-.075.075-.112.163T5 13.125q0 .2.15.363t.35.162q.1 0 .2-.05t.15-.1l3.4-3.4l1.4 1.4l-3.375 3.4q-.075.075-.112.163t-.038.187q0 .2.15.35t.35.15q.1 0 .2-.05t.15-.1l3.4-3.375l1.4 1.4l-3.375 3.4q-.075.05-.112.15t-.038.2q0 .2.15.35t.35.15q.1 0 .188-.038t.162-.112l3.4-3.375l1.4 1.4l-3.4 3.4q-.075.075-.112.162t-.038.188q0 .2.163.35t.362.15m-.025 2q-.925 0-1.637-.612t-.838-1.538q-.85-.125-1.425-.7t-.7-1.425q-.85-.125-1.412-.712T5.15 15.6q-.95-.125-1.55-.825t-.6-1.65q0-.5.188-.962t.537-.813l5.8-5.775L12.8 8.85q.05.075.15.113t.2.037q.225 0 .375-.137t.15-.363q0-.1-.038-.2t-.112-.15L9.95 4.575q-.275-.3-.637-.437T8.55 4q-.375 0-.75.138t-.675.437L3.6 8.125q-.225.225-.375.525t-.2.6t0 .613t.2.587l-1.45 1.45q-.425-.575-.625-1.262T1 9.25t.35-1.362t.825-1.188L5.7 3.175Q6.3 2.6 7.038 2.3T8.55 2t1.513.3t1.312.875l.275.275l.275-.275q.6-.575 1.338-.875t1.512-.3t1.513.3t1.312.875L21.825 7.4q.575.575.875 1.325t.3 1.525t-.3 1.513t-.875 1.312l-8.2 8.175q-.35.35-.812.55t-.963.2M9.375 8"
/>
</svg>
</template>
<script>
import icon from "../../mixins/icon";
export default {
name: "Welcome",
mixins: [icon],
};
</script>

View file

@ -95,6 +95,7 @@ export default {
chargedEnergy: Number,
charging: Boolean,
vehicleClimaterActive: Boolean,
vehicleWelcomeActive: Boolean,
connected: Boolean,
currency: String,
effectiveLimitSoc: Number,

View file

@ -232,5 +232,8 @@ const planProjectedEnd = getFutureTime(5, 43);
vehicleClimaterActive
/>
</Variant>
<Variant title="welcome charge">
<VehicleStatus connected charging vehicleWelcomeActive />
</Variant>
</Story>
</template>

View file

@ -66,6 +66,15 @@
>
<ClimaterIcon />
</div>
<div
v-if="vehicleWelcomeActive"
ref="vehicleWelcome"
data-bs-toggle="tooltip"
class="entry"
data-testid="vehicle-status-welcome"
>
<WelcomeIcon />
</div>
<!-- smart cost -->
<button
@ -140,6 +149,7 @@ import VehicleMinSocIcon from "./MaterialIcon/VehicleMinSoc.vue";
import SunDownIcon from "./MaterialIcon/SunDown.vue";
import SunUpIcon from "./MaterialIcon/SunUp.vue";
import Tooltip from "bootstrap/js/dist/tooltip";
import WelcomeIcon from "./MaterialIcon/Welcome.vue";
export default {
name: "VehicleStatus",
@ -152,6 +162,7 @@ export default {
VehicleMinSocIcon,
SunDownIcon,
SunUpIcon,
WelcomeIcon,
},
mixins: [formatter],
props: {
@ -183,6 +194,7 @@ export default {
tariffCo2: Number,
tariffGrid: Number,
vehicleClimaterActive: Boolean,
vehicleWelcomeActive: Boolean,
vehicleLimitSoc: Number,
},
emits: ["open-loadpoint-settings", "open-minsoc-settings", "open-plan-modal"],
@ -194,6 +206,7 @@ export default {
planActiveTooltip: null,
minSocTooltip: null,
vehicleClimaterTooltip: null,
vehicleWelcomeTooltip: null,
smartCostTooltip: null,
vehicleLimitTooltip: null,
};
@ -205,6 +218,7 @@ export default {
this.updatePhaseTooltip();
this.updatePvTooltip();
this.updateVehicleClimaterTooltip();
this.updateVehicleWelcomeTooltip();
this.updateSmartCostTooltip();
this.updateVehicleLimitTooltip();
},
@ -227,6 +241,9 @@ export default {
vehicleClimaterTooltipContent() {
this.$nextTick(this.updateVehicleClimaterTooltip);
},
vehicleWelcomeTooltipContent() {
this.$nextTick(this.updateVehicleWelcomeTooltip);
},
smartCostTooltipContent() {
this.$nextTick(this.updateSmartCostTooltip);
},
@ -399,8 +416,17 @@ export default {
return "";
},
vehicleClimaterTooltipContent() {
if (!this.vehicleClimaterActive) {
return "";
}
return this.$t("main.vehicleStatus.climating");
},
vehicleWelcomeTooltipContent() {
if (!this.vehicleWelcomeActive) {
return "";
}
return this.$t("main.vehicleStatus.welcome");
},
chargerStatus() {
const t = (key, data) => {
if (this.heating) {
@ -428,91 +454,6 @@ export default {
return t("charging");
}
return t("connected");
},
message: function () {
const t = (key, data) => {
if (this.heating) {
// check for special heating status translation
const name = `main.heatingStatus.${key}`;
if (this.$te(name, DEFAULT_LOCALE)) {
return this.$t(name, data);
}
}
return this.$t(`main.vehicleStatus.${key}`, data);
};
if (!this.connected) {
return t("disconnected");
}
// min charge active
if (this.minSoc > 0 && this.vehicleSoc < this.minSoc) {
return t("minCharge", { soc: this.fmtPercentage(this.minSoc) });
}
// plan
if (!this.chargingPlanDisabled && this.effectivePlanTime) {
if (this.planActive && this.charging) {
return t("targetChargeActive");
}
if (this.planActive && this.enabled) {
return t("targetChargeWaitForVehicle");
}
if (this.planProjectedStart) {
return t("targetChargePlanned", {
time: this.fmtAbsoluteDate(new Date(this.planProjectedStart)),
});
}
}
// clean or cheap energy
if (this.charging && this.smartCostActive) {
return this.smartCostType === CO2_TYPE
? t("cleanEnergyCharging", {
co2: this.fmtCo2Short(this.tariffCo2),
limit: this.fmtCo2Short(this.smartCostLimit),
})
: t("cheapEnergyCharging", {
price: this.fmtPricePerKWh(this.tariffGrid, this.currency, true),
limit: this.fmtPricePerKWh(this.smartCostLimit, this.currency, true),
});
}
if (this.pvTimerActive && !this.enabled && this.pvAction === "enable") {
return t("pvEnable", {
remaining: this.fmtDuration(this.pvRemainingInterpolated),
});
}
if (this.enabled && this.vehicleClimaterActive) {
return t("climating");
}
if (this.enabled && !this.charging) {
if (this.vehicleLimitSoc > 0 && this.vehicleSoc >= this.vehicleLimitSoc - 1) {
return t("vehicleLimitReached", {
soc: this.fmtPercentage(this.vehicleLimitSoc),
});
}
return t("waitForVehicle");
}
if (this.pvTimerActive && this.charging && this.pvAction === "disable") {
return t("pvDisable", {
remaining: this.fmtDuration(this.pvRemainingInterpolated),
});
}
if (this.phaseTimerActive) {
return t(this.phaseAction, {
remaining: this.fmtDuration(this.phaseRemainingInterpolated),
});
}
if (this.charging) {
return t("charging");
}
return t("connected");
},
},
@ -587,6 +528,13 @@ export default {
this.$refs.vehicleClimater
);
},
updateVehicleWelcomeTooltip() {
this.vehicleWelcomeTooltip = this.updateTooltip(
this.vehicleWelcomeTooltip,
this.vehicleWelcomeTooltipContent,
this.$refs.vehicleWelcome
);
},
updateSmartCostTooltip() {
this.smartCostTooltip = this.updateTooltip(
this.smartCostTooltip,

View file

@ -36,6 +36,7 @@ import (
// Keba is an api.Charger implementation
type Keba struct {
*embed
log *util.Logger
conn *modbus.Connection
}
@ -68,15 +69,20 @@ func init() {
// NewKebaFromConfig creates a new Keba ModbusTCP charger
func NewKebaFromConfig(other map[string]interface{}) (api.Charger, error) {
cc := modbus.TcpSettings{
ID: 255,
cc := struct {
embed `mapstructure:",squash"`
modbus.TcpSettings
}{
TcpSettings: modbus.TcpSettings{
ID: 255,
},
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
wb, err := NewKeba(cc.URI, cc.ID)
wb, err := NewKeba(cc.embed, cc.URI, cc.ID)
if err != nil {
return nil, err
}
@ -129,7 +135,7 @@ func NewKebaFromConfig(other map[string]interface{}) (api.Charger, error) {
}
// NewKeba creates a new charger
func NewKeba(uri string, slaveID uint8) (*Keba, error) {
func NewKeba(embed embed, uri string, slaveID uint8) (*Keba, error) {
conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
if err != nil {
return nil, err
@ -143,8 +149,9 @@ func NewKeba(uri string, slaveID uint8) (*Keba, error) {
conn.Logger(log.TRACE)
wb := &Keba{
log: log,
conn: conn,
embed: &embed,
log: log,
conn: conn,
}
return wb, err

View file

@ -19,8 +19,8 @@ func NewAdapter(lp loadpoint.API, c *Coordinator) API {
}
}
func (a *adapter) GetVehicles() []api.Vehicle {
return a.c.GetVehicles()
func (a *adapter) GetVehicles(availableOnly bool) []api.Vehicle {
return a.c.GetVehicles(availableOnly)
}
func (a *adapter) Owner(v api.Vehicle) loadpoint.API {

View file

@ -7,8 +7,8 @@ import (
// API is the coordinator API
type API interface {
// GetVehicles returns the list of all vehicles
GetVehicles() []api.Vehicle
// GetVehicles returns the list of all vehicles, filtered by availability
GetVehicles(availableOnly bool) []api.Vehicle
// Owner returns the loadpoint that currently owns the vehicle
Owner(api.Vehicle) loadpoint.API

View file

@ -1,7 +1,6 @@
package coordinator
import (
"slices"
"sync"
"github.com/evcc-io/evcc/api"
@ -27,11 +26,18 @@ func New(log *util.Logger, vehicles []api.Vehicle) *Coordinator {
}
// GetVehicles returns the list of all vehicles
func (c *Coordinator) GetVehicles() []api.Vehicle {
func (c *Coordinator) GetVehicles(availableOnly bool) []api.Vehicle {
c.mu.RLock()
defer c.mu.RUnlock()
return slices.Clone(c.vehicles)
res := make([]api.Vehicle, 0, len(c.vehicles))
for _, v := range c.vehicles {
if _, tracked := c.tracked[v]; !availableOnly || availableOnly && !tracked {
res = append(res, v)
}
}
return res
}
// Owner returns the loadpoint that currently owns the vehicle

View file

@ -12,7 +12,7 @@ func NewDummy() API {
return new(dummy)
}
func (a *dummy) GetVehicles() []api.Vehicle {
func (a *dummy) GetVehicles(_ bool) []api.Vehicle {
return nil
}

View file

@ -60,7 +60,7 @@ const (
PlanSoc = "planSoc" // charge plan soc goal
PlanActive = "planActive" // charge plan has determined current slot to be an active slot
PlanProjectedStart = "planProjectedStart" // charge plan start time (earliest slot)
PlanProjectedEnd = "planProjectedEnd" //charge plan ends (end of last slot)
PlanProjectedEnd = "planProjectedEnd" // charge plan ends (end of last slot)
PlanOverrun = "planOverrun" // charge plan goal not reachable in time
// remote control
@ -76,4 +76,5 @@ const (
VehicleSoc = "vehicleSoc" // vehicle soc
VehicleLimitSoc = "vehicleLimitSoc" // vehicle api soc limit
VehicleClimaterActive = "vehicleClimaterActive" // vehicle climater active
VehicleWelcomeActive = "vehicleWelcomeActive" // vehicle might need welcome charge
)

View file

@ -5,6 +5,7 @@ import (
"fmt"
"math"
"reflect"
"slices"
"strings"
"sync"
"testing"
@ -885,7 +886,7 @@ func (lp *Loadpoint) charging() bool {
return lp.GetStatus() == api.StatusC
}
// charging returns the EVs charging state
// setStatus updates the internal charging state according to EV
func (lp *Loadpoint) setStatus(status api.ChargeStatus) {
lp.Lock()
defer lp.Unlock()
@ -993,10 +994,12 @@ func statusEvents(prevStatus, status api.ChargeStatus) []string {
}
// updateChargerStatus updates charger status and detects car connected/disconnected events
func (lp *Loadpoint) updateChargerStatus() error {
func (lp *Loadpoint) updateChargerStatus() (bool, error) {
var welcomeCharge bool
status, err := lp.charger.Status()
if err != nil {
return fmt.Errorf("charger status: %w", err)
return false, fmt.Errorf("charger status: %w", err)
}
lp.log.DEBUG.Printf("charger status: %s", status)
@ -1011,6 +1014,20 @@ func (lp *Loadpoint) updateChargerStatus() error {
if prevStatus != api.StatusNone {
switch ev {
case evVehicleConnect:
welcomeCharge = lp.chargerHasFeature(api.WelcomeCharge)
// Enable charging on connect if any available vehicle requires it.
// We're using the PV timer to disable after the welcome
if !welcomeCharge {
for _, v := range lp.availableVehicles() {
if slices.Contains(v.Features(), api.WelcomeCharge) {
welcomeCharge = true
lp.log.DEBUG.Printf("welcome charge: %s", v.Title())
break
}
}
}
lp.pushEvent(evVehicleConnect)
case evVehicleDisconnect:
lp.pushEvent(evVehicleDisconnect)
@ -1022,7 +1039,7 @@ func (lp *Loadpoint) updateChargerStatus() error {
lp.bus.Publish(evChargeCurrent, lp.chargeCurrent)
}
return nil
return welcomeCharge, nil
}
// effectiveCurrent returns the currently effective charging current
@ -1635,11 +1652,13 @@ func (lp *Loadpoint) Update(sitePower float64, smartCostActive bool, smartCostNe
lp.PublishEffectiveValues()
// read and publish status
if err := lp.updateChargerStatus(); err != nil {
welcomeCharge, err := lp.updateChargerStatus()
if err != nil {
lp.log.ERROR.Println(err)
return
}
lp.publish(keys.VehicleWelcomeActive, welcomeCharge)
lp.publish(keys.Connected, lp.connected())
lp.publish(keys.Charging, lp.charging())
@ -1664,9 +1683,6 @@ func (lp *Loadpoint) Update(sitePower float64, smartCostActive bool, smartCostNe
return
}
// check if car connected and ready for charging
var err error
// track if remote disabled is actually active
remoteDisabled := loadpoint.RemoteEnable
@ -1691,7 +1707,11 @@ func (lp *Loadpoint) Update(sitePower float64, smartCostActive bool, smartCostNe
fallthrough
case mode == api.ModeOff:
err = lp.setLimit(0)
var current float64
if welcomeCharge {
current = lp.effectiveMinCurrent()
}
err = lp.setLimit(current)
// minimum or target charging
case lp.minSocNotReached() || plannerActive:
@ -1726,6 +1746,11 @@ func (lp *Loadpoint) Update(sitePower float64, smartCostActive bool, smartCostNe
targetCurrent = lp.effectiveMinCurrent()
}
if targetCurrent == 0 && welcomeCharge {
targetCurrent = lp.effectiveMinCurrent()
lp.resetPVTimer()
}
// Sunny Home Manager
if lp.remoteControlled(loadpoint.RemoteSoftDisable) {
remoteDisabled = loadpoint.RemoteSoftDisable

View file

@ -20,12 +20,20 @@ const (
vehicleDetectDuration = 10 * time.Minute
)
// availableVehicles is the slice of vehicles from the coordinator that are available
func (lp *Loadpoint) availableVehicles() []api.Vehicle {
if lp.coordinator == nil {
return nil
}
return lp.coordinator.GetVehicles(true)
}
// coordinatedVehicles is the slice of vehicles from the coordinator
func (lp *Loadpoint) coordinatedVehicles() []api.Vehicle {
if lp.coordinator == nil {
return nil
}
return lp.coordinator.GetVehicles()
return lp.coordinator.GetVehicles(false)
}
// setVehicleIdentifier updated the vehicle id as read from the charger

View file

@ -541,6 +541,7 @@ unknown = ""
vehicleLimit = "Fahrzeuglimit."
vehicleLimitReached = "Fahrzeuglimit erreicht."
waitForVehicle = "Ladebereit. Warte auf Fahrzeug …"
welcome = "Kurze initiale Ladung zur Bestätigung der Verbindung."
[notifications]
dismissAll = "Meldungen entfernen"

View file

@ -539,6 +539,7 @@ unknown = ""
vehicleLimit = "Vehicle limit."
vehicleLimitReached = "Vehicle limit reached."
waitForVehicle = "Ready. Waiting for vehicle…"
welcome = "Short initial charge to confirm connection."
[notifications]
dismissAll = "Dismiss all"

View file

@ -19,6 +19,11 @@ params:
- name: modbus
choice: ["tcpip"]
id: 255
- name: welcomecharge
advanced: true
render: |
type: keba-modbus
{{- include "modbus" . }}
{{- if .welcomecharge }}
features: ["welcomecharge"]
{{- end }}

View file

@ -15,6 +15,8 @@ params:
# - EU
default: EU
advanced: true
- name: welcomecharge
advanced: true
render: |
type: bmw
{{ include "vehicle-base" . }}
@ -22,3 +24,6 @@ render: |
{{- if ne .region "EU" }}
region: {{ .region }}
{{- end }}
{{- if .welcomecharge }}
features: ["welcomecharge"]
{{- end }}

View file

@ -4,7 +4,12 @@ products:
params:
- preset: vehicle-base
- preset: vehicle-identify
- name: welcomecharge
advanced: true
render: |
type: dacia
{{ include "vehicle-base" . }}
{{ include "vehicle-identify" . }}
{{- if .welcomecharge }}
features: ["welcomecharge"]
{{- end }}

View file

@ -9,6 +9,8 @@ params:
- name: pin
mask: true
- preset: vehicle-identify
- name: welcomecharge
advanced: true
render: |
type: fiat
{{ include "vehicle-base" . }}
@ -16,3 +18,6 @@ render: |
pin: {{ .pin }} # mandatory to deep refresh Soc
{{- end }}
{{ include "vehicle-identify" . }}
{{- if .welcomecharge }}
features: ["welcomecharge"]
{{- end }}

View file

@ -15,6 +15,8 @@ params:
# - EU
default: EU
advanced: true
- name: welcomecharge
advanced: true
render: |
type: mini
{{ include "vehicle-base" . }}
@ -22,3 +24,6 @@ render: |
{{- if ne .region "EU" }}
region: {{ .region }}
{{- end }}
{{- if .welcomecharge }}
features: ["welcomecharge"]
{{- end }}

View file

@ -6,7 +6,12 @@ products:
params:
- preset: vehicle-base
- preset: vehicle-identify
- name: welcomecharge
advanced: true
render: |
type: cupra
{{ include "vehicle-base" . }}
{{ include "vehicle-identify" . }}
{{- if .welcomecharge }}
features: ["welcomecharge"]
{{- end }}

View file

@ -6,7 +6,12 @@ products:
params:
- preset: vehicle-base
- preset: vehicle-identify
- name: welcomecharge
advanced: true
render: |
type: smart-hello
{{ include "vehicle-base" . }}
{{ include "vehicle-identify" . }}
{{- if .welcomecharge }}
features: ["welcomecharge"]
{{- end }}

View file

@ -289,6 +289,18 @@ params:
help:
en: The AIN is printed on the type label on the back of the device. Embed it in double quotes in case of leading zeroes.
de: Die AIN ist auf dem Typenschild auf der Geräterückseite aufgedruckt. Bei führenden Nullen bitte in doppelte Hochkommata setzen.
- name: welcomecharge
type: bool
description:
en: Charge on connection
de: Laden bei Verbindung
help:
en: |
Charger will enable charging for short time when vehicle is connected, irrespective of configured charge mode.
This is useful for vehicles that require power supply when connecting.
de: |
Wallbox gibt kurzzeitige Ladefreigabe bei Fahrzeugverbindung. Das ermöglicht es Fahrzeugen, die eine Stromversorgung beim Anschließen benötigen,
einen Fehlerzustand zu vermeiden.
presets:
vehicle-base: