Support chargers without vehicle as "integrated" devices (#6316)

This commit is contained in:
andig 2023-02-21 15:14:13 +01:00 • committed by GitHub
parent 5a925211d0
commit d35170955e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 255 additions and 98 deletions

View file

@ -137,9 +137,9 @@ type Authorizer interface {
type Vehicle interface {
Battery
BatteryCapacity
IconDescriber
Title() string
SetTitle(string)
Icon() string
Phases() int
Identifiers() []string
OnIdentified() ActionConfig
@ -210,10 +210,14 @@ type AuthProvider interface {
LogoutHandler() http.HandlerFunc
}
// IconDescriber optionally provides an icon
type IconDescriber interface {
Icon() string
}
// FeatureDescriber optionally provides a list of supported non-api features
type FeatureDescriber interface {
Features() []Feature
Has(Feature) bool
}
// CsvWriter converts to csv

View file

@ -15,4 +15,5 @@ const (
_ Feature = iota
Offline
CoarseCurrent
IntegratedDevice
)

View file

@ -7,11 +7,11 @@ import (
"strings"
)
const _FeatureName = "OfflineCoarseCurrent"
const _FeatureName = "OfflineCoarseCurrentIntegratedDevice"
var _FeatureIndex = [...]uint8{0, 7, 20}
var _FeatureIndex = [...]uint8{0, 7, 20, 36}
const _FeatureLowerName = "offlinecoarsecurrent"
const _FeatureLowerName = "offlinecoarsecurrentintegrateddevice"
func (i Feature) String() string {
i -= 1
@ -27,20 +27,24 @@ func _FeatureNoOp() {
var x [1]struct{}
_ = x[Offline-(1)]
_ = x[CoarseCurrent-(2)]
_ = x[IntegratedDevice-(3)]
}
var _FeatureValues = []Feature{Offline, CoarseCurrent}
var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice}
var _FeatureNameToValueMap = map[string]Feature{
_FeatureName[0:7]: Offline,
_FeatureLowerName[0:7]: Offline,
_FeatureName[7:20]: CoarseCurrent,
_FeatureLowerName[7:20]: CoarseCurrent,
_FeatureName[0:7]: Offline,
_FeatureLowerName[0:7]: Offline,
_FeatureName[7:20]: CoarseCurrent,
_FeatureLowerName[7:20]: CoarseCurrent,
_FeatureName[20:36]: IntegratedDevice,
_FeatureLowerName[20:36]: IntegratedDevice,
}
var _FeatureNames = []string{
_FeatureName[0:7],
_FeatureName[7:20],
_FeatureName[20:36],
}
// FeatureString retrieves an enum value from the enum constants string name.

View file

@ -54,5 +54,14 @@ const state = reactive({
:chargePower="0"
/>
</Variant>
<Variant title="charger icon, no vehicle">
<Loadpoint
v-bind="state"
chargerIcon="heater"
title="Heating device with long name"
mode="now"
chargerFeatureIntegratedDevice
/>
</Variant>
</Story>
</template>

View file

@ -1,9 +1,16 @@
<template>
<div class="loadpoint pt-4 pb-2 px-3 px-sm-4 mx-2 mx-sm-0">
<div class="loadpoint d-flex flex-column pt-4 pb-2 px-3 px-sm-4 mx-2 mx-sm-0">
<div class="d-block d-sm-flex justify-content-between align-items-center mb-3">
<div class="d-flex justify-content-between align-items-center mb-3 text-truncate">
<h3 class="me-2 mb-0 text-truncate">
{{ title || $t("main.loadpoint.fallbackName") }}
<h3 class="me-2 mb-0 text-truncate d-flex">
<VehicleIcon
v-if="chargerIcon"
:name="chargerIcon"
class="me-2 flex-shrink-0"
/>
<div class="text-truncate">
{{ title || $t("main.loadpoint.fallbackName") }}
</div>
</h3>
<LoadpointSettingsButton
v-if="settingsButtonVisible"
@ -91,6 +98,7 @@
</div>
<hr class="divider" />
<Vehicle
class="flex-grow-1 d-flex flex-column justify-content-end"
v-bind="vehicle"
@target-soc-updated="setTargetSoc"
@target-energy-updated="setTargetEnergy"
@ -114,6 +122,7 @@ import formatter from "../mixins/formatter";
import collector from "../mixins/collector";
import LoadpointSettingsButton from "./LoadpointSettingsButton.vue";
import LoadpointSettingsModal from "./LoadpointSettingsModal.vue";
import VehicleIcon from "./VehicleIcon";
export default {
name: "Loadpoint",
@ -124,6 +133,7 @@ export default {
LabelAndValue,
LoadpointSettingsButton,
LoadpointSettingsModal,
VehicleIcon,
},
mixins: [formatter, collector],
props: {
@ -140,6 +150,10 @@ export default {
chargeDuration: Number,
charging: Boolean,
// charger
chargerFeatureIntegratedDevice: Boolean,
chargerIcon: String,
// vehicle
connected: Boolean,
// charging: Boolean,
@ -194,6 +208,9 @@ export default {
};
},
computed: {
integratedDevice: function () {
return this.chargerFeatureIntegratedDevice;
},
phasesProps: function () {
return this.collectProps(Phases);
},

View file

@ -0,0 +1,47 @@
<script setup>
import Loadpoints from "./Loadpoints.vue";
import { reactive } from "vue";
function loadpoint(opts) {
const base = {
id: 0,
pvConfigured: true,
chargePower: 2800,
chargedEnergy: 11e3,
chargeDuration: 95 * 60,
vehiclePresent: true,
vehicleTitle: "Tesla Model 3",
enabled: true,
connected: true,
mode: "pv",
charging: true,
vehicleSoc: 66,
targetSoc: 90,
chargeCurrent: 7,
minCurrent: 6,
maxCurrent: 16,
activePhases: 2,
};
return { ...base, ...opts };
}
const state = reactive({
vehicles: [],
loadpoints: [
loadpoint({ title: "Carport" }),
loadpoint({
title: "Water Heater",
chargerFeatureIntegratedDevice: true,
chargerIcon: "waterheater",
}),
],
});
</script>
<template>
<Story>
<Variant title="standard">
<Loadpoints v-bind="state" />
</Variant>
</Story>
</template>

View file

@ -104,7 +104,7 @@ export default {
},
vehicleIcons: function () {
if (this.activeLoadpointsCount) {
return this.activeLoadpoints.map((lp) => lp.vehicleIcon || "car");
return this.activeLoadpoints.map((lp) => lp.chargerIcon || lp.vehicleIcon || "car");
}
return ["car"];
},

View file

@ -1,11 +1,12 @@
<template>
<div class="vehicle pt-4">
<VehicleTitle
v-if="!integratedDevice"
v-bind="vehicleTitleProps"
@change-vehicle="changeVehicle"
@remove-vehicle="removeVehicle"
/>
<VehicleStatus v-if="!parked" v-bind="vehicleStatus" class="mb-2" />
<VehicleStatus v-bind="vehicleStatus" class="mb-2" />
<VehicleSoc
v-bind="vehicleSocProps"
class="mt-2 mb-4"
@ -84,6 +85,7 @@ export default {
props: {
id: [String, Number],
connected: Boolean,
integratedDevice: Boolean,
vehiclePresent: Boolean,
vehicleSoc: Number,
vehicleTargetSoc: Number,
@ -107,7 +109,6 @@ export default {
phaseRemainingInterpolated: Number,
pvAction: String,
pvRemainingInterpolated: Number,
parked: Boolean,
vehicles: Array,
},
emits: [

View file

@ -54,7 +54,6 @@ export default {
vehicleTitle: String,
vehicleIcon: String,
vehicleDetectionActive: Boolean,
parked: Boolean,
connected: Boolean,
vehicles: { type: Array, default: () => [] },
},
@ -64,13 +63,13 @@ export default {
if (this.vehicleDetectionActive) {
return "refresh";
}
if (this.connected || this.parked) {
if (this.connected) {
return "vehicle";
}
return null;
},
name() {
if (this.vehiclePresent || this.parked) {
if (this.vehiclePresent) {
return this.vehicleTitle || this.$t("main.vehicle.fallbackName");
}
if (this.connected) {

View file

@ -10,6 +10,7 @@ import (
// Charger is an api.Charger implementation with configurable getters and setters.
type Charger struct {
*embed
statusG func() (string, error)
enabledG func() (bool, error)
enableS func(bool) error
@ -25,9 +26,11 @@ func init() {
// NewConfigurableFromConfig creates a new configurable charger
func NewConfigurableFromConfig(other map[string]interface{}) (api.Charger, error) {
var cc struct {
embed `mapstructure:",squash"`
Status, Enable, Enabled, MaxCurrent provider.Config
Identify, Phases1p3p *provider.Config
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
@ -54,6 +57,8 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Charger, error
c, err := NewConfigurable(status, enabled, enable, maxcurrent)
c.embed = &cc.embed
// decorator phases
var phases1p3p func(int) error
if err == nil && cc.Phases1p3p != nil {
@ -82,6 +87,7 @@ func NewConfigurable(
maxCurrentS func(int64) error,
) (*Charger, error) {
c := &Charger{
embed: new(embed),
statusG: statusG,
enabledG: enabledG,
enableS: enableS,

24
charger/embed.go Normal file
View file

@ -0,0 +1,24 @@
package charger
import (
"github.com/evcc-io/evcc/api"
)
type embed struct {
Icon_ string `mapstructure:"icon"`
Features_ []api.Feature `mapstructure:"features"`
}
var _ api.IconDescriber = (*embed)(nil)
// Icon implements the api.Vehicle interface
func (v *embed) Icon() string {
return v.Icon_
}
var _ api.FeatureDescriber = (*embed)(nil)
// Features implements the api.FeatureDescriber interface
func (v *embed) Features() []api.Feature {
return v.Features_
}

View file

@ -25,6 +25,7 @@ func init() {
// NewFritzDECTFromConfig creates a fritzdect charger from generic config
func NewFritzDECTFromConfig(other map[string]interface{}) (api.Charger, error) {
var cc struct {
embed `mapstructure:",squash"`
fritzdect.Settings `mapstructure:",squash"`
StandbyPower float64
}
@ -33,18 +34,18 @@ func NewFritzDECTFromConfig(other map[string]interface{}) (api.Charger, error) {
return nil, err
}
return NewFritzDECT(cc.URI, cc.AIN, cc.User, cc.Password, cc.StandbyPower)
return NewFritzDECT(cc.embed, cc.URI, cc.AIN, cc.User, cc.Password, cc.StandbyPower)
}
// NewFritzDECT creates a new connection with standbypower for charger
func NewFritzDECT(uri, ain, user, password string, standbypower float64) (*FritzDECT, error) {
func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower float64) (*FritzDECT, error) {
conn, err := fritzdect.NewConnection(uri, ain, user, password)
c := &FritzDECT{
conn: conn,
}
c.switchSocket = NewSwitchSocket(c.Enabled, c.conn.CurrentPower, standbypower)
c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
return c, err
}

View file

@ -19,6 +19,7 @@ func init() {
// NewCCUFromConfig creates a Homematic charger from generic config
func NewCCUFromConfig(other map[string]interface{}) (api.Charger, error) {
var cc struct {
embed `mapstructure:",squash"`
URI string
Device string
MeterChannel string
@ -32,18 +33,18 @@ func NewCCUFromConfig(other map[string]interface{}) (api.Charger, error) {
return nil, err
}
return NewCCU(cc.URI, cc.Device, cc.MeterChannel, cc.SwitchChannel, cc.User, cc.Password, cc.StandbyPower)
return NewCCU(cc.embed, cc.URI, cc.Device, cc.MeterChannel, cc.SwitchChannel, cc.User, cc.Password, cc.StandbyPower)
}
// NewCCU creates a new connection with standbypower for charger
func NewCCU(uri, deviceid, meterid, switchid, user, password string, standbypower float64) (*CCU, error) {
func NewCCU(embed embed, uri, deviceid, meterid, switchid, user, password string, standbypower float64) (*CCU, error) {
conn, err := homematic.NewConnection(uri, deviceid, meterid, switchid, user, password)
c := &CCU{
conn: conn,
}
c.switchSocket = NewSwitchSocket(c.Enabled, c.conn.CurrentPower, standbypower)
c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
return c, err
}

View file

@ -19,15 +19,15 @@ func init() {
// MyStrom charger implementation
type MyStrom struct {
conn *mystrom.Connection
*switchSocket
cache time.Duration
reportCache provider.Cacheable[mystrom.Report]
conn *mystrom.Connection
reportG provider.Cacheable[mystrom.Report]
}
// NewMyStromFromConfig creates a myStrom charger from generic config
func NewMyStromFromConfig(other map[string]interface{}) (api.Charger, error) {
cc := struct {
embed `mapstructure:",squash"`
URI string
StandbyPower float64
Cache time.Duration
@ -40,26 +40,24 @@ func NewMyStromFromConfig(other map[string]interface{}) (api.Charger, error) {
}
c := &MyStrom{
conn: mystrom.NewConnection(cc.URI),
cache: cc.Cache,
conn: mystrom.NewConnection(cc.URI),
}
c.switchSocket = NewSwitchSocket(c.Enabled, c.conn.CurrentPower, cc.StandbyPower)
c.reportCache = provider.ResettableCached(c.conn.Report, c.cache)
c.switchSocket = NewSwitchSocket(&cc.embed, c.Enabled, c.conn.CurrentPower, cc.StandbyPower)
c.reportG = provider.ResettableCached(c.conn.Report, cc.Cache)
return c, nil
}
// Enabled implements the api.Charger interface
func (c *MyStrom) Enabled() (bool, error) {
res, err := c.reportCache.Get()
res, err := c.reportG.Get()
return res.Relay, err
}
// Enable implements the api.Charger interface
func (c *MyStrom) Enable(enable bool) error {
// reset cache
c.reportCache.Reset()
c.reportG.Reset()
onoff := map[bool]int{false: 0, true: 1}
return c.conn.Request(fmt.Sprintf("relay?state=%d", onoff[enable]))

View file

@ -21,6 +21,7 @@ func init() {
// NewShellyFromConfig creates a Shelly charger from generic config
func NewShellyFromConfig(other map[string]interface{}) (api.Charger, error) {
var cc struct {
embed `mapstructure:",squash"`
URI string
User string
Password string
@ -32,11 +33,11 @@ func NewShellyFromConfig(other map[string]interface{}) (api.Charger, error) {
return nil, err
}
return NewShelly(cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower)
return NewShelly(cc.embed, cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower)
}
// NewShelly creates Shelly charger
func NewShelly(uri, user, password string, channel int, standbypower float64) (*Shelly, error) {
func NewShelly(embed embed, uri, user, password string, channel int, standbypower float64) (*Shelly, error) {
conn, err := shelly.NewConnection(uri, user, password, channel)
if err != nil {
return nil, err
@ -46,7 +47,7 @@ func NewShelly(uri, user, password string, channel int, standbypower float64) (*
conn: conn,
}
c.switchSocket = NewSwitchSocket(c.Enabled, c.conn.CurrentPower, standbypower)
c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
return c, nil
}

View file

@ -5,17 +5,20 @@ import "github.com/evcc-io/evcc/api"
// switchSocket implements the api.Charger Status and CurrentPower methods
// using basic generic switch socket functions
type switchSocket struct {
*embed
enabled func() (bool, error)
currentPower func() (float64, error)
standbypower float64
}
func NewSwitchSocket(
embed *embed,
enabled func() (bool, error),
currentPower func() (float64, error),
standbypower float64,
) *switchSocket {
return &switchSocket{
embed: embed,
enabled: enabled,
currentPower: currentPower,
standbypower: standbypower,

View file

@ -19,6 +19,7 @@ func init() {
// NewTapoFromConfig creates a Tapo charger from generic config
func NewTapoFromConfig(other map[string]interface{}) (api.Charger, error) {
var cc struct {
embed `mapstructure:",squash"`
URI string
User string
Password string
@ -29,11 +30,11 @@ func NewTapoFromConfig(other map[string]interface{}) (api.Charger, error) {
return nil, err
}
return NewTapo(cc.URI, cc.User, cc.Password, cc.StandbyPower)
return NewTapo(cc.embed, cc.URI, cc.User, cc.Password, cc.StandbyPower)
}
// NewTapo creates Tapo charger
func NewTapo(uri, user, password string, standbypower float64) (*Tapo, error) {
func NewTapo(embed embed, uri, user, password string, standbypower float64) (*Tapo, error) {
conn, err := tapo.NewConnection(uri, user, password)
if err != nil {
return nil, err
@ -43,7 +44,7 @@ func NewTapo(uri, user, password string, standbypower float64) (*Tapo, error) {
conn: conn,
}
c.switchSocket = NewSwitchSocket(c.Enabled, c.conn.CurrentPower, standbypower)
c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
return c, nil
}

View file

@ -29,6 +29,7 @@ func init() {
// NewTasmotaFromConfig creates a Tasmota charger from generic config
func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) {
cc := struct {
embed `mapstructure:",squash"`
URI string
User string
Password string
@ -42,11 +43,11 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) {
return nil, err
}
return NewTasmota(cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower)
return NewTasmota(cc.embed, cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower)
}
// NewTasmota creates Tasmota charger
func NewTasmota(uri, user, password string, channel int, standbypower float64) (*Tasmota, error) {
func NewTasmota(embed embed, uri, user, password string, channel int, standbypower float64) (*Tasmota, error) {
conn, err := tasmota.NewConnection(uri, user, password, channel)
if err != nil {
return nil, err
@ -57,7 +58,7 @@ func NewTasmota(uri, user, password string, channel int, standbypower float64) (
channel: channel,
}
c.switchSocket = NewSwitchSocket(c.Enabled, c.conn.CurrentPower, standbypower)
c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
return c, c.channelExists(channel)
}

View file

@ -23,6 +23,7 @@ func init() {
// NewTPLinkFromConfig creates a TP-Link charger from generic config
func NewTPLinkFromConfig(other map[string]interface{}) (api.Charger, error) {
var cc struct {
embed `mapstructure:",squash"`
URI string
StandbyPower float64
}
@ -35,11 +36,11 @@ func NewTPLinkFromConfig(other map[string]interface{}) (api.Charger, error) {
return nil, errors.New("missing uri")
}
return NewTPLink(cc.URI, cc.StandbyPower)
return NewTPLink(cc.embed, cc.URI, cc.StandbyPower)
}
// NewTPLink creates TP-Link charger
func NewTPLink(uri string, standbypower float64) (*TPLink, error) {
func NewTPLink(embed embed, uri string, standbypower float64) (*TPLink, error) {
conn, err := tplink.NewConnection(uri)
if err != nil {
return nil, err
@ -49,7 +50,7 @@ func NewTPLink(uri string, standbypower float64) (*TPLink, error) {
conn: conn,
}
c.switchSocket = NewSwitchSocket(c.Enabled, c.conn.CurrentPower, standbypower)
c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
return c, nil
}

View file

@ -7,16 +7,17 @@ const (
phasesEnabled = "phasesEnabled" // enabled phases (1/3)
phasesActive = "phasesActive" // active phases as used by vehicle (1/2/3)
vehicleDetectionActive = "vehicleDetectionActive" // vehicle detection is active (bool)
chargerIcon = "chargerIcon" // charger icon for ui
vehicleOdometer = "vehicleOdometer" // vehicle odometer
vehicleRange = "vehicleRange" // vehicle range
vehicleSoc = "vehicleSoc" // vehicle soc
vehicleTargetSoc = "vehicleTargetSoc" // vehicle soc limit
vehicleCapacity = "vehicleCapacity" // vehicle battery capacity
vehicleIcon = "vehicleIcon" // vehicle icon for ui
vehiclePresent = "vehiclePresent" // vehicle detected
vehicleTitle = "vehicleTitle" // vehicle title
vehicleCapacity = "vehicleCapacity" // vehicle battery capacity
vehicleDetectionActive = "vehicleDetectionActive" // vehicle detection active
vehicleIcon = "vehicleIcon" // vehicle icon for ui
vehicleOdometer = "vehicleOdometer" // vehicle odometer
vehiclePresent = "vehiclePresent" // vehicle detected
vehicleRange = "vehicleRange" // vehicle range
vehicleSoc = "vehicleSoc" // vehicle soc
vehicleTargetSoc = "vehicleTargetSoc" // vehicle soc limit
vehicleTitle = "vehicleTitle" // vehicle title
minCurrent = "minCurrent" // charger min current
maxCurrent = "maxCurrent" // charger max current

View file

@ -436,7 +436,9 @@ func (lp *Loadpoint) evVehicleConnectHandler() {
}
// set default or start detection
lp.vehicleDefaultOrDetect()
if !lp.chargerHasFeature(api.IntegratedDevice) {
lp.vehicleDefaultOrDetect()
}
// immediately allow pv mode activity
lp.elapsePVTimer()
@ -561,6 +563,18 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
lp.publishTimer(phaseTimer, 0, timerInactive)
lp.publishTimer(pvTimer, 0, timerInactive)
// charger features
for _, f := range []api.Feature{api.IntegratedDevice} {
lp.publishChargerFeature(f)
}
// charger icon
if c, ok := lp.charger.(api.IconDescriber); ok {
lp.publish(chargerIcon, c.Icon())
} else {
lp.publish(chargerIcon, nil)
}
// assign and publish default vehicle
if lp.defaultVehicle != nil {
lp.setActiveVehicle(lp.defaultVehicle)
@ -1037,14 +1051,6 @@ func (lp *Loadpoint) pvScalePhases(availablePower, minCurrent, maxCurrent float6
return false
}
// 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()
}
// TODO move up to timer functions
func (lp *Loadpoint) publishTimer(name string, delay time.Duration, action string) {
timer := lp.pvTimer
@ -1335,24 +1341,23 @@ func (lp *Loadpoint) socPollAllowed() bool {
return false
}
// checks if the connected charger can provide Soc to the connected vehicle
func (lp *Loadpoint) socProvidedByCharger() bool {
if charger, ok := lp.charger.(api.Battery); ok {
if _, err := charger.Soc(); err == nil {
return true
}
}
return false
}
// publish state of charge, remaining charge duration and range
func (lp *Loadpoint) publishSocAndRange() {
soc, err := lp.chargerSoc()
// guard for socEstimator removed by api
if lp.socEstimator == nil {
// This is a workaround for heaters. Without vehicle, the soc estimator is not initialized.
// We need to check if the charger can provide soc and use it if available.
if err == nil {
lp.vehicleSoc = soc
lp.publish(vehicleSoc, lp.vehicleSoc)
}
return
}
if lp.socPollAllowed() || lp.socProvidedByCharger() {
if err == nil || lp.socPollAllowed() {
lp.socUpdated = lp.clock.Now()
f, err := lp.socEstimator.Soc(lp.getChargedEnergy())
@ -1366,7 +1371,7 @@ func (lp *Loadpoint) publishSocAndRange() {
return
}
lp.vehicleSoc = math.Trunc(f)
lp.vehicleSoc = f
lp.log.DEBUG.Printf("vehicle soc: %.0f%%", lp.vehicleSoc)
lp.publish(vehicleSoc, lp.vehicleSoc)
@ -1463,7 +1468,7 @@ func (lp *Loadpoint) Update(sitePower float64, batteryBuffered bool) {
lp.publish("enabled", lp.enabled)
// identify connected vehicle
if lp.connected() {
if lp.connected() && !lp.chargerHasFeature(api.IntegratedDevice) {
// read identity and run associated action
lp.identifyVehicle()

32
core/loadpoint_charger.go Normal file
View file

@ -0,0 +1,32 @@
package core
import (
"github.com/evcc-io/evcc/api"
"golang.org/x/exp/slices"
)
// chargerHasFeature checks availability of charger feature
func (lp *Loadpoint) chargerHasFeature(f api.Feature) bool {
c, ok := lp.charger.(api.FeatureDescriber)
if ok {
ok = slices.Contains(c.Features(), f)
}
return ok
}
// publishChargerFeature publishes availability of charger features
func (lp *Loadpoint) publishChargerFeature(f api.Feature) {
c, ok := lp.charger.(api.FeatureDescriber)
if ok {
ok = slices.Contains(c.Features(), f)
}
lp.publish("chargerFeature"+f.String(), ok)
}
// chargerSoc returns charger soc if available
func (lp *Loadpoint) chargerSoc() (float64, error) {
if c, ok := lp.charger.(api.Battery); ok {
return c.Soc()
}
return 0, api.ErrNotAvailable
}

View file

@ -9,6 +9,7 @@ import (
"github.com/evcc-io/evcc/core/db"
"github.com/evcc-io/evcc/core/soc"
"github.com/evcc-io/evcc/provider"
"golang.org/x/exp/slices"
)
const (
@ -16,6 +17,14 @@ const (
vehicleDetectDuration = 10 * time.Minute
)
// 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()
}
// setVehicleIdentifier updated the vehicle id as read from the charger
func (lp *Loadpoint) setVehicleIdentifier(id string) {
if lp.vehicleIdentifier != id {
@ -191,20 +200,20 @@ func (lp *Loadpoint) unpublishVehicle() {
lp.setRemainingEnergy(0)
lp.setRemainingDuration(0)
lp.vehiclePublishFeature(api.Offline)
lp.publishVehicleFeature(api.Offline)
}
// vehicleHasFeature checks availability of vehicle feature
func (lp *Loadpoint) vehicleHasFeature(f api.Feature) bool {
v, ok := lp.vehicle.(api.FeatureDescriber)
if ok {
ok = v.Has(f)
ok = slices.Contains(v.Features(), f)
}
return ok
}
// vehiclePublishFeature availability of vehicle features
func (lp *Loadpoint) vehiclePublishFeature(f api.Feature) {
// publishVehicleFeature availability of vehicle features
func (lp *Loadpoint) publishVehicleFeature(f api.Feature) {
lp.publish("vehicleFeature"+f.String(), lp.vehicleHasFeature(f))
}

View file

@ -2,7 +2,6 @@ package vehicle
import (
"github.com/evcc-io/evcc/api"
"golang.org/x/exp/slices"
)
type embed struct {
@ -25,11 +24,6 @@ func (v *embed) SetTitle(title string) {
v.Title_ = title
}
// Icon implements the api.Vehicle interface
func (v *embed) Icon() string {
return v.Icon_
}
// Capacity implements the api.Vehicle interface
func (v *embed) Capacity() float64 {
return v.Capacity_
@ -50,14 +44,16 @@ func (v *embed) OnIdentified() api.ActionConfig {
return v.OnIdentify
}
var _ api.IconDescriber = (*embed)(nil)
// Icon implements the api.Vehicle interface
func (v *embed) Icon() string {
return v.Icon_
}
var _ api.FeatureDescriber = (*embed)(nil)
// Features implements the api.FeatureDescriber interface
func (v *embed) Features() []api.Feature {
return v.Features_
}
// Features implements the api.FeatureDescriber interface
func (v *embed) Has(f api.Feature) bool {
return slices.Contains(v.Features_, f)
}

View file

@ -68,11 +68,6 @@ func (v *Wrapper) Features() []api.Feature {
return []api.Feature{api.Offline}
}
// Features implements the api.FeatureDescriber interface
func (v *Wrapper) Has(f api.Feature) bool {
return f == api.Offline
}
var _ api.Battery = (*Wrapper)(nil)
// Soc implements the api.Battery interface