Support chargers without vehicle as "integrated" devices (#6316)
This commit is contained in:
parent
5a925211d0
commit
d35170955e
25 changed files with 255 additions and 98 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -15,4 +15,5 @@ const (
|
|||
_ Feature = iota
|
||||
Offline
|
||||
CoarseCurrent
|
||||
IntegratedDevice
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
47
assets/js/components/Loadpoints.story.vue
Normal file
47
assets/js/components/Loadpoints.story.vue
Normal 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>
|
||||
|
|
@ -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"];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
24
charger/embed.go
Normal 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_
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
32
core/loadpoint_charger.go
Normal 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
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue