Batteries: consolidate measurements api (BC) (#24887)
This commit is contained in:
parent
15071f4ac9
commit
15fd774062
18 changed files with 224 additions and 132 deletions
|
|
@ -266,7 +266,7 @@ import collector from "@/mixins/collector.js";
|
|||
import api from "@/api";
|
||||
import settings from "@/settings";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { BatteryMeter, SelectOption, CURRENCY, Forecast } from "@/types/evcc";
|
||||
import type { Battery, SelectOption, CURRENCY, Forecast } from "@/types/evcc";
|
||||
import { SMART_COST_TYPE } from "@/types/evcc";
|
||||
|
||||
export default defineComponent({
|
||||
|
|
@ -276,10 +276,9 @@ export default defineComponent({
|
|||
props: {
|
||||
bufferSoc: { type: Number, default: 100 },
|
||||
prioritySoc: { type: Number, default: 0 },
|
||||
batterySoc: { type: Number, default: 0 },
|
||||
bufferStartSoc: { type: Number, default: 0 },
|
||||
batteryDischargeControl: Boolean,
|
||||
battery: { type: Array as PropType<BatteryMeter[]>, default: () => [] },
|
||||
battery: { type: Object as PropType<Battery> },
|
||||
batteryGridChargeLimit: { type: Number, default: null },
|
||||
smartCostAvailable: Boolean,
|
||||
smartCostType: String as PropType<SMART_COST_TYPE>,
|
||||
|
|
@ -300,6 +299,12 @@ export default defineComponent({
|
|||
usageTabActive() {
|
||||
return !this.gridTabActive;
|
||||
},
|
||||
batterySoc() {
|
||||
return this.battery?.soc ?? 0;
|
||||
},
|
||||
batteryDevices() {
|
||||
return this.battery?.devices ?? [];
|
||||
},
|
||||
priorityOptions() {
|
||||
const options = [];
|
||||
for (let i = 100; i >= 0; i -= 5) {
|
||||
|
|
@ -312,7 +317,7 @@ export default defineComponent({
|
|||
return options;
|
||||
},
|
||||
controllable() {
|
||||
return this.battery.some(({ controllable }) => controllable);
|
||||
return this.batteryDevices.some(({ controllable }) => controllable);
|
||||
},
|
||||
gridChargePossible() {
|
||||
return this.controllable && this.isModalVisible && this.smartCostAvailable;
|
||||
|
|
@ -368,13 +373,13 @@ export default defineComponent({
|
|||
return this.prioritySoc;
|
||||
},
|
||||
batteryDetails() {
|
||||
if (!Array.isArray(this.battery)) {
|
||||
if (!this.batteryDevices.length) {
|
||||
return;
|
||||
}
|
||||
return this.battery
|
||||
const multipleBatteries = this.batteryDevices.length > 1;
|
||||
return this.batteryDevices
|
||||
.filter(({ capacity }) => capacity > 0)
|
||||
.map(({ soc = 0, capacity }) => {
|
||||
const multipleBatteries = this.battery.length > 1;
|
||||
const energy = this.fmtWh(
|
||||
(capacity / 100) * soc * 1e3,
|
||||
POWER_UNIT.KW,
|
||||
|
|
|
|||
|
|
@ -129,9 +129,9 @@
|
|||
<template v-if="batteryGridChargeLimitSet" #subline>
|
||||
<div class="d-none d-md-block"> </div>
|
||||
</template>
|
||||
<template v-if="battery.length > 1" #expanded>
|
||||
<template v-if="hasMultipleBatteries" #expanded>
|
||||
<EnergyflowEntry
|
||||
v-for="(b, index) in battery"
|
||||
v-for="(b, index) in batteryDevices"
|
||||
:key="index"
|
||||
:name="b.title || genericBatteryTitle(index)"
|
||||
:details="b.soc"
|
||||
|
|
@ -271,9 +271,9 @@
|
|||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="battery.length > 1" #expanded>
|
||||
<template v-if="hasMultipleBatteries" #expanded>
|
||||
<EnergyflowEntry
|
||||
v-for="(b, index) in battery"
|
||||
v-for="(b, index) in batteryDevices"
|
||||
:key="index"
|
||||
:name="b.title || genericBatteryTitle(index)"
|
||||
:details="b.soc"
|
||||
|
|
@ -316,7 +316,7 @@ import collector from "@/mixins/collector.js";
|
|||
import { defineComponent, type PropType } from "vue";
|
||||
import {
|
||||
SMART_COST_TYPE,
|
||||
type BatteryMeter,
|
||||
type Battery,
|
||||
type Meter,
|
||||
type CURRENCY,
|
||||
type Forecast,
|
||||
|
|
@ -342,9 +342,7 @@ export default defineComponent({
|
|||
pvPower: { type: Number, default: 0 },
|
||||
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
|
||||
batteryConfigured: { type: Boolean },
|
||||
battery: { type: Array as PropType<BatteryMeter[]>, default: () => [] },
|
||||
batteryPower: { type: Number, default: 0 },
|
||||
batterySoc: { type: Number, default: 0 },
|
||||
battery: { type: Object as PropType<Battery> },
|
||||
batteryDischargeControl: { type: Boolean },
|
||||
batteryGridChargeLimit: { type: Number },
|
||||
batteryGridChargeActive: { type: Boolean },
|
||||
|
|
@ -388,6 +386,18 @@ export default defineComponent({
|
|||
pvProduction() {
|
||||
return Math.abs(this.pvPower);
|
||||
},
|
||||
batterySoc() {
|
||||
return this.battery?.soc;
|
||||
},
|
||||
batteryPower() {
|
||||
return this.battery?.power ?? 0;
|
||||
},
|
||||
batteryDevices() {
|
||||
return this.battery?.devices ?? [];
|
||||
},
|
||||
hasMultipleBatteries() {
|
||||
return this.batteryDevices.length > 1;
|
||||
},
|
||||
batteryDischarge() {
|
||||
return this.dischargePower(this.batteryPower);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -128,13 +128,13 @@ export default defineComponent({
|
|||
selfBattery: { type: Number, default: 0 },
|
||||
pvExport: { type: Number, default: 0 },
|
||||
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
|
||||
batterySoc: { type: Number },
|
||||
batteryCharge: { type: Number, default: 0 },
|
||||
batteryDischarge: { type: Number, default: 0 },
|
||||
batteryHold: { type: Boolean, default: false },
|
||||
batteryGridCharge: { type: Boolean, default: false },
|
||||
pvProduction: { type: Number, default: 0 },
|
||||
homePower: { type: Number, default: 0 },
|
||||
batterySoc: { type: Number, default: 0 },
|
||||
powerUnit: { type: String as PropType<POWER_UNIT>, default: POWER_UNIT.KW },
|
||||
inPower: { type: Number, default: 0 },
|
||||
outPower: { type: Number, default: 0 },
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ import WelcomeIcons from "./WelcomeIcons.vue";
|
|||
import { defineComponent, type PropType } from "vue";
|
||||
import type {
|
||||
AuthProviders,
|
||||
BatteryMeter,
|
||||
Battery,
|
||||
Meter,
|
||||
CURRENCY,
|
||||
Forecast,
|
||||
|
|
@ -128,13 +128,11 @@ export default defineComponent({
|
|||
pv: { type: Array as PropType<Meter[]>, default: () => [] },
|
||||
aux: { type: Array as PropType<Meter[]>, default: () => [] },
|
||||
ext: { type: Array as PropType<Meter[]>, default: () => [] },
|
||||
batteryPower: Number,
|
||||
batterySoc: Number,
|
||||
batteryDischargeControl: Boolean,
|
||||
batteryGridChargeLimit: { type: Number, default: null },
|
||||
batteryGridChargeActive: Boolean,
|
||||
batteryMode: String,
|
||||
battery: { type: Array as PropType<BatteryMeter[]>, default: () => [] },
|
||||
battery: { type: Object as PropType<Battery> },
|
||||
gridCurrents: Array,
|
||||
prioritySoc: Number,
|
||||
bufferSoc: Number,
|
||||
|
|
@ -174,8 +172,11 @@ export default defineComponent({
|
|||
orderedVisibleLoadpoints() {
|
||||
return this.loadpoints.filter((lp) => lp.visible);
|
||||
},
|
||||
batterySoc() {
|
||||
return this.battery?.soc;
|
||||
},
|
||||
batteryConfigured() {
|
||||
return this.battery?.length > 0;
|
||||
return this.battery && this.battery.devices.length > 0;
|
||||
},
|
||||
pvConfigured() {
|
||||
return this.pv?.length > 0;
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ import { logout, isLoggedIn, openLoginModal } from "../Auth/auth";
|
|||
import { isApp, sendToApp } from "@/utils/native";
|
||||
import { isUserConfigError } from "@/utils/fatal";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { FatalError, Sponsor, EvOpt, AuthProviders } from "@/types/evcc";
|
||||
import type { FatalError, Sponsor, EvOpt, AuthProviders, Battery } from "@/types/evcc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TopNavigation",
|
||||
|
|
@ -163,7 +163,7 @@ export default defineComponent({
|
|||
authProviders: { type: Object as PropType<AuthProviders>, default: () => ({}) },
|
||||
sponsor: { type: Object as PropType<Sponsor>, default: () => ({}) },
|
||||
forecast: Object,
|
||||
battery: Array,
|
||||
battery: { type: Object as PropType<Battery> },
|
||||
evopt: { type: Object as PropType<EvOpt>, required: false },
|
||||
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },
|
||||
},
|
||||
|
|
@ -176,7 +176,7 @@ export default defineComponent({
|
|||
},
|
||||
computed: {
|
||||
batteryConfigured() {
|
||||
return this.battery?.length;
|
||||
return this.battery && this.battery.devices.length;
|
||||
},
|
||||
providers() {
|
||||
return Object.entries(this.authProviders)
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export interface State {
|
|||
version?: string;
|
||||
system?: string;
|
||||
timezone?: string;
|
||||
battery?: BatteryMeter[];
|
||||
battery?: Battery;
|
||||
pv?: Meter[];
|
||||
aux?: Meter[];
|
||||
ext?: Meter[];
|
||||
|
|
@ -490,6 +490,13 @@ export interface Meter {
|
|||
energy?: number;
|
||||
}
|
||||
|
||||
export interface Battery {
|
||||
power: number;
|
||||
capacity: number;
|
||||
soc: number;
|
||||
devices: BatteryMeter[];
|
||||
}
|
||||
|
||||
export interface BatteryMeter extends Meter {
|
||||
soc: number;
|
||||
controllable: boolean;
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export default defineComponent({
|
|||
return store.state.version;
|
||||
},
|
||||
batteryModalAvailabe() {
|
||||
return store.state.battery?.length;
|
||||
return store.state.battery?.devices.length;
|
||||
},
|
||||
showRoutes() {
|
||||
return this.state.startupCompleted;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ const (
|
|||
AuxMeters = "auxMeters"
|
||||
|
||||
// battery settings
|
||||
BatteryCapacity = "batteryCapacity"
|
||||
BatteryDischargeControl = "batteryDischargeControl"
|
||||
BatteryGridChargeLimit = "batteryGridChargeLimit"
|
||||
BatteryGridChargeActive = "batteryGridChargeActive"
|
||||
|
|
@ -48,11 +47,8 @@ const (
|
|||
BufferStartSoc = "bufferStartSoc"
|
||||
|
||||
// battery status
|
||||
Battery = "battery"
|
||||
BatteryEnergy = "batteryEnergy"
|
||||
BatteryMode = "batteryMode"
|
||||
BatteryPower = "batteryPower"
|
||||
BatterySoc = "batterySoc"
|
||||
Battery = "battery"
|
||||
BatteryMode = "batteryMode"
|
||||
|
||||
// external battery control
|
||||
BatteryModeExternal = "batteryModeExternal"
|
||||
|
|
|
|||
106
core/site.go
106
core/site.go
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/evcc-io/evcc/core/session"
|
||||
"github.com/evcc-io/evcc/core/site"
|
||||
"github.com/evcc-io/evcc/core/soc"
|
||||
"github.com/evcc-io/evcc/core/types"
|
||||
"github.com/evcc-io/evcc/core/vehicle"
|
||||
"github.com/evcc-io/evcc/push"
|
||||
"github.com/evcc-io/evcc/server/db"
|
||||
|
|
@ -48,27 +49,6 @@ type updater interface {
|
|||
Update(sitePower, batteryBoostPower float64, consumption, feedin api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
|
||||
}
|
||||
|
||||
// measurement is used as slice element for publishing structured data
|
||||
type measurement struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Power float64 `json:"power"`
|
||||
Energy float64 `json:"energy,omitempty"`
|
||||
Powers []float64 `json:"powers,omitempty"`
|
||||
Currents []float64 `json:"currents,omitempty"`
|
||||
ExcessDCPower float64 `json:"excessdcpower,omitempty"`
|
||||
Capacity *float64 `json:"capacity,omitempty"`
|
||||
Soc *float64 `json:"soc,omitempty"`
|
||||
Controllable *bool `json:"controllable,omitempty"`
|
||||
}
|
||||
|
||||
var _ api.TitleDescriber = (*measurement)(nil)
|
||||
|
||||
// GetTitle implements api.TitleDescriber interface for InfluxDB tagging
|
||||
func (m measurement) GetTitle() string {
|
||||
return m.Title
|
||||
}
|
||||
|
||||
var _ site.API = (*Site)(nil)
|
||||
|
||||
// Site is the main configuration container. A site can host multiple loadpoints.
|
||||
|
|
@ -112,16 +92,14 @@ type Site struct {
|
|||
householdSlotStart time.Time
|
||||
|
||||
// cached state
|
||||
gridPower float64 // Grid power
|
||||
pvPower float64 // PV power
|
||||
excessDCPower float64 // PV excess DC charge power (hybrid only)
|
||||
auxPower float64 // Aux power
|
||||
batteryPower float64 // Battery power (charge negative, discharge positive)
|
||||
batterySoc float64 // Battery soc
|
||||
batteryCapacity float64 // Battery capacity
|
||||
batteryMode api.BatteryMode // Battery mode (runtime only, not persisted)
|
||||
batteryModeExternal api.BatteryMode // Battery mode (external, runtime only, not persisted)
|
||||
batteryModeExternalTimer time.Time // Battery mode timer for external control
|
||||
gridPower float64 // Grid power
|
||||
pvPower float64 // PV power
|
||||
excessDCPower float64 // PV excess DC charge power (hybrid only)
|
||||
auxPower float64 // Aux power
|
||||
battery types.BatteryState // Battery cached and published state
|
||||
batteryMode api.BatteryMode // Battery mode (runtime only, not persisted)
|
||||
batteryModeExternal api.BatteryMode // Battery mode (external, runtime only, not persisted)
|
||||
batteryModeExternalTimer time.Time // Battery mode timer for external control
|
||||
}
|
||||
|
||||
// MetersConfig contains the site's meter configuration
|
||||
|
|
@ -501,8 +479,8 @@ func (site *Site) clearPlanLocks() {
|
|||
}
|
||||
}
|
||||
|
||||
func (site *Site) collectMeters(key string, meters []config.Device[api.Meter]) []measurement {
|
||||
mm := make([]measurement, len(meters))
|
||||
func (site *Site) collectMeters(key string, meters []config.Device[api.Meter]) []types.Measurement {
|
||||
mm := make([]types.Measurement, len(meters))
|
||||
|
||||
fun := func(i int, dev config.Device[api.Meter]) {
|
||||
meter := dev.Instance()
|
||||
|
|
@ -529,7 +507,7 @@ func (site *Site) collectMeters(key string, meters []config.Device[api.Meter]) [
|
|||
}
|
||||
|
||||
props := deviceProperties(dev)
|
||||
mm[i] = measurement{
|
||||
mm[i] = types.Measurement{
|
||||
Title: props.Title,
|
||||
Icon: props.Icon,
|
||||
Power: power,
|
||||
|
|
@ -573,13 +551,13 @@ func (site *Site) updatePvMeters() {
|
|||
}
|
||||
}
|
||||
|
||||
site.pvPower = lo.SumBy(mm, func(m measurement) float64 {
|
||||
site.pvPower = lo.SumBy(mm, func(m types.Measurement) float64 {
|
||||
return max(0, m.Power)
|
||||
})
|
||||
site.excessDCPower = lo.SumBy(mm, func(m measurement) float64 {
|
||||
site.excessDCPower = lo.SumBy(mm, func(m types.Measurement) float64 {
|
||||
return math.Abs(m.ExcessDCPower)
|
||||
})
|
||||
totalEnergy := lo.SumBy(mm, func(m measurement) float64 {
|
||||
totalEnergy := lo.SumBy(mm, func(m types.Measurement) float64 {
|
||||
return m.Energy
|
||||
})
|
||||
|
||||
|
|
@ -622,7 +600,7 @@ func (site *Site) updatePvMeters() {
|
|||
}
|
||||
|
||||
// updateBatteryMeters updates battery meters
|
||||
func (site *Site) updateBatteryMeters() []measurement {
|
||||
func (site *Site) updateBatteryMeters() []types.Measurement {
|
||||
if len(site.batteryMeters) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -652,7 +630,7 @@ func (site *Site) updateBatteryMeters() []measurement {
|
|||
mm[i].Controllable = lo.ToPtr(controllable)
|
||||
}
|
||||
|
||||
batterySocAcc := lo.SumBy(mm, func(m measurement) float64 {
|
||||
batterySocAcc := lo.SumBy(mm, func(m types.Measurement) float64 {
|
||||
if m.Soc == nil {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -662,7 +640,7 @@ func (site *Site) updateBatteryMeters() []measurement {
|
|||
}
|
||||
return *m.Soc
|
||||
})
|
||||
totalCapacity := lo.SumBy(mm, func(m measurement) float64 {
|
||||
totalCapacity := lo.SumBy(mm, func(m types.Measurement) float64 {
|
||||
if m.Capacity == nil {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -673,27 +651,23 @@ func (site *Site) updateBatteryMeters() []measurement {
|
|||
if totalCapacity == 0 {
|
||||
totalCapacity = float64(len(site.batteryMeters))
|
||||
}
|
||||
site.batterySoc = batterySocAcc / totalCapacity
|
||||
site.batteryCapacity = totalCapacity
|
||||
site.battery.Soc = batterySocAcc / totalCapacity
|
||||
site.battery.Capacity = totalCapacity
|
||||
|
||||
site.batteryPower = lo.SumBy(mm, func(m measurement) float64 {
|
||||
site.battery.Power = lo.SumBy(mm, func(m types.Measurement) float64 {
|
||||
return m.Power
|
||||
})
|
||||
totalEnergy := lo.SumBy(mm, func(m measurement) float64 {
|
||||
site.battery.Energy = lo.SumBy(mm, func(m types.Measurement) float64 {
|
||||
return m.Energy
|
||||
})
|
||||
|
||||
if len(site.batteryMeters) > 1 {
|
||||
site.log.DEBUG.Printf("battery power: %.0fW", site.batteryPower)
|
||||
site.log.DEBUG.Printf("battery soc: %.0f%%", math.Round(site.batterySoc))
|
||||
site.log.DEBUG.Printf("battery power: %.0fW", site.battery.Power)
|
||||
site.log.DEBUG.Printf("battery soc: %.0f%%", math.Round(site.battery.Soc))
|
||||
}
|
||||
|
||||
site.publish(keys.BatteryCapacity, site.batteryCapacity)
|
||||
site.publish(keys.BatterySoc, site.batterySoc)
|
||||
|
||||
site.publish(keys.BatteryPower, site.batteryPower)
|
||||
site.publish(keys.BatteryEnergy, totalEnergy)
|
||||
site.publish(keys.Battery, mm)
|
||||
site.battery.Devices = mm
|
||||
site.publish(keys.Battery, site.battery)
|
||||
|
||||
return mm
|
||||
}
|
||||
|
|
@ -705,7 +679,7 @@ func (site *Site) updateAuxMeters() {
|
|||
}
|
||||
|
||||
mm := site.collectMeters("aux", site.auxMeters)
|
||||
site.auxPower = lo.SumBy(mm, func(m measurement) float64 {
|
||||
site.auxPower = lo.SumBy(mm, func(m types.Measurement) float64 {
|
||||
return m.Power
|
||||
})
|
||||
|
||||
|
|
@ -733,7 +707,7 @@ func (site *Site) updateGridMeter() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
var mm measurement
|
||||
var mm types.Measurement
|
||||
|
||||
if res, err := backoff.RetryWithData(site.gridMeter.CurrentPower, modbus.Backoff()); err == nil {
|
||||
mm.Power = res
|
||||
|
|
@ -782,7 +756,7 @@ func (site *Site) updateGridMeter() error {
|
|||
func (site *Site) updateMeters() error {
|
||||
var eg errgroup.Group
|
||||
|
||||
var battery []measurement
|
||||
var battery []types.Measurement
|
||||
|
||||
eg.Go(func() error { site.updatePvMeters(); return nil })
|
||||
eg.Go(func() error { battery = site.updateBatteryMeters(); return nil })
|
||||
|
|
@ -841,12 +815,12 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
|
|||
// allow using PV as estimate for grid power
|
||||
if site.gridMeter == nil {
|
||||
site.gridPower = totalChargePower - site.pvPower
|
||||
site.publish(keys.Grid, measurement{Power: site.gridPower})
|
||||
site.publish(keys.Grid, types.Measurement{Power: site.gridPower})
|
||||
}
|
||||
|
||||
// ensure safe default for residual power
|
||||
residualPower := site.GetResidualPower()
|
||||
if len(site.batteryMeters) > 0 && site.batterySoc < site.prioritySoc && residualPower <= 0 {
|
||||
if len(site.batteryMeters) > 0 && site.battery.Soc < site.prioritySoc && residualPower <= 0 {
|
||||
residualPower = 100 // Wsite.publish(keys.PvPower,
|
||||
}
|
||||
|
||||
|
|
@ -861,7 +835,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
|
|||
}
|
||||
|
||||
// honour battery priority
|
||||
batteryPower := site.batteryPower
|
||||
batteryPower := site.battery.Power
|
||||
excessDCPower := site.excessDCPower
|
||||
|
||||
// handed to loadpoint
|
||||
|
|
@ -872,14 +846,14 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
|
|||
defer site.RUnlock()
|
||||
|
||||
// if battery is charging below prioritySoc give it priority
|
||||
if site.batterySoc < site.prioritySoc && batteryPower < 0 {
|
||||
site.log.DEBUG.Printf("battery has priority at soc %.0f%% (< %.0f%%)", site.batterySoc, site.prioritySoc)
|
||||
if site.battery.Soc < site.prioritySoc && batteryPower < 0 {
|
||||
site.log.DEBUG.Printf("battery has priority at soc %.0f%% (< %.0f%%)", site.battery.Soc, site.prioritySoc)
|
||||
batteryPower = 0
|
||||
excessDCPower = 0
|
||||
} else {
|
||||
// if battery is above bufferSoc allow using it for charging
|
||||
batteryBuffered = site.bufferSoc > 0 && site.batterySoc > site.bufferSoc
|
||||
batteryStart = site.bufferStartSoc > 0 && site.batterySoc > site.bufferStartSoc
|
||||
batteryBuffered = site.bufferSoc > 0 && site.battery.Soc > site.bufferSoc
|
||||
batteryStart = site.bufferStartSoc > 0 && site.battery.Soc > site.bufferStartSoc
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -969,7 +943,7 @@ func (site *Site) update(lp updater) {
|
|||
|
||||
if sitePower, batteryBuffered, batteryStart, err := site.sitePower(totalChargePower, flexiblePower); err == nil {
|
||||
// ignore negative pvPower values as that means it is not an energy source but consumption
|
||||
homePower := site.gridPower + max(0, site.pvPower) + site.batteryPower - totalChargePower
|
||||
homePower := site.gridPower + max(0, site.pvPower) + site.battery.Power - totalChargePower
|
||||
homePower = max(homePower, 0)
|
||||
site.publish(keys.HomePower, homePower)
|
||||
|
||||
|
|
@ -979,14 +953,14 @@ func (site *Site) update(lp updater) {
|
|||
|
||||
// add battery charging power to homePower to ignore all consumption which does not occur on loadpoints
|
||||
// fix for: https://github.com/evcc-io/evcc/issues/11032
|
||||
nonChargePower := homePower + max(0, -site.batteryPower)
|
||||
nonChargePower := homePower + max(0, -site.battery.Power)
|
||||
greenShareHome := site.greenShare(0, homePower)
|
||||
greenShareLoadpoints := site.greenShare(nonChargePower, nonChargePower+totalChargePower)
|
||||
|
||||
// TODO
|
||||
if lp != nil {
|
||||
lp.Update(
|
||||
sitePower, max(0, site.batteryPower), consumption, feedin, batteryBuffered, batteryStart,
|
||||
sitePower, max(0, site.battery.Power), consumption, feedin, batteryBuffered, batteryStart,
|
||||
greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints),
|
||||
)
|
||||
}
|
||||
|
|
@ -1033,9 +1007,9 @@ func (site *Site) prepare() {
|
|||
site.publish(keys.GridConfigured, site.gridMeter != nil)
|
||||
site.publish(keys.Grid, api.Meter(nil))
|
||||
site.publish(keys.Pv, []api.Meter{})
|
||||
site.publish(keys.Battery, []api.Meter{})
|
||||
site.publish(keys.Aux, []api.Meter{})
|
||||
site.publish(keys.Ext, []api.Meter{})
|
||||
site.publish(keys.Battery, nil)
|
||||
site.publish(keys.PrioritySoc, site.prioritySoc)
|
||||
site.publish(keys.BufferSoc, site.bufferSoc)
|
||||
site.publish(keys.BufferStartSoc, site.bufferStartSoc)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/loadpoint"
|
||||
"github.com/evcc-io/evcc/core/metrics"
|
||||
"github.com/evcc-io/evcc/core/types"
|
||||
"github.com/evcc-io/evcc/tariff"
|
||||
"github.com/evcc-io/evcc/util/config"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
|
|
@ -54,7 +55,7 @@ type responseDetails struct {
|
|||
|
||||
const slotsPerHour = float64(time.Hour / tariff.SlotDuration)
|
||||
|
||||
func (site *Site) optimizerUpdateAsync(battery []measurement) {
|
||||
func (site *Site) optimizerUpdateAsync(battery []types.Measurement) {
|
||||
var err error
|
||||
|
||||
if time.Since(updated) < 2*time.Minute {
|
||||
|
|
@ -81,7 +82,7 @@ func (site *Site) optimizerUpdateAsync(battery []measurement) {
|
|||
err = site.optimizerUpdate(battery)
|
||||
}
|
||||
|
||||
func (site *Site) optimizerUpdate(battery []measurement) error {
|
||||
func (site *Site) optimizerUpdate(battery []types.Measurement) error {
|
||||
uri := os.Getenv("EVOPT_URI")
|
||||
if uri == "" {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ type dailyDetails struct {
|
|||
// - the current green share, calculated for the part of the consumption between powerFrom and powerTo
|
||||
// the consumption below powerFrom will get the available green power first
|
||||
func (site *Site) greenShare(powerFrom float64, powerTo float64) float64 {
|
||||
greenPower := math.Max(0, site.pvPower) + math.Max(0, site.batteryPower)
|
||||
greenPower := math.Max(0, site.pvPower) + math.Max(0, site.battery.Power)
|
||||
greenPowerAvailable := math.Max(0, greenPower-powerFrom)
|
||||
|
||||
power := powerTo - powerFrom
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/types"
|
||||
"github.com/evcc-io/evcc/server/db"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/config"
|
||||
|
|
@ -105,9 +106,11 @@ func TestGreenShare(t *testing.T) {
|
|||
t.Log(tc.title)
|
||||
|
||||
s := &Site{
|
||||
gridPower: tc.grid,
|
||||
pvPower: tc.pv,
|
||||
batteryPower: tc.battery,
|
||||
gridPower: tc.grid,
|
||||
pvPower: tc.pv,
|
||||
battery: types.BatteryState{
|
||||
Power: tc.battery,
|
||||
},
|
||||
}
|
||||
|
||||
totalPower := tc.grid + tc.pv + max(0, tc.battery)
|
||||
|
|
|
|||
32
core/types/types.go
Normal file
32
core/types/types.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package types
|
||||
|
||||
import "github.com/evcc-io/evcc/api"
|
||||
|
||||
// Measurement is the device measurements struct
|
||||
type Measurement struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Power float64 `json:"power"`
|
||||
Energy float64 `json:"energy,omitempty"`
|
||||
Powers []float64 `json:"powers,omitempty"`
|
||||
Currents []float64 `json:"currents,omitempty"`
|
||||
ExcessDCPower float64 `json:"excessdcpower,omitempty"`
|
||||
Capacity *float64 `json:"capacity,omitempty"`
|
||||
Soc *float64 `json:"soc,omitempty"`
|
||||
Controllable *bool `json:"controllable,omitempty"`
|
||||
}
|
||||
|
||||
var _ api.TitleDescriber = (*Measurement)(nil)
|
||||
|
||||
// GetTitle implements api.TitleDescriber interface for InfluxDB tagging
|
||||
func (m Measurement) GetTitle() string {
|
||||
return m.Title
|
||||
}
|
||||
|
||||
type BatteryState struct {
|
||||
Power float64 `json:"power"`
|
||||
Energy float64 `json:"energy,omitempty"`
|
||||
Capacity float64 `json:"capacity,omitempty"`
|
||||
Soc float64 `json:"soc"`
|
||||
Devices []Measurement `json:"devices,omitempty" influxdb:"battery"`
|
||||
}
|
||||
|
|
@ -35,9 +35,25 @@ func jsonDecoder(r io.Reader) *json.Decoder {
|
|||
return dec
|
||||
}
|
||||
|
||||
// omitEmpty returns true if struct field is omitempty
|
||||
func omitEmpty(f reflect.StructField) bool {
|
||||
tag := f.Tag.Get("json")
|
||||
values := strings.Split(tag, ",")
|
||||
return slices.Contains(values, "omitempty")
|
||||
// jsonOmitEmpty returns true if struct field is omitempty
|
||||
func jsonOmitEmpty(f reflect.StructField) bool {
|
||||
return tagAttribute("json", "omitempty", f)
|
||||
}
|
||||
|
||||
// tagValue returns the given tag's primary value
|
||||
func tagValue(key string, f reflect.StructField) string {
|
||||
if tag := f.Tag.Get(key); tag != "" {
|
||||
return strings.Split(tag, ",")[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// tagAttribute returns the given tag's primary value
|
||||
func tagAttribute(key, attr string, f reflect.StructField) bool {
|
||||
if tag := f.Tag.Get(key); tag != "" {
|
||||
if attrs := strings.Split(tag, ","); len(attrs) > 1 {
|
||||
return slices.Contains(attrs[1:], attr)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ type pointWriter interface {
|
|||
WritePoint(point *write.Point)
|
||||
}
|
||||
|
||||
func influxTagValue(f reflect.StructField) string {
|
||||
return tagValue("influxdb", f)
|
||||
}
|
||||
|
||||
// writePoint asynchronously writes a point to influx
|
||||
func (m *Influx) writePoint(writer pointWriter, key string, fields map[string]any, tags map[string]string) {
|
||||
m.log.TRACE.Printf("write %s=%v (%v)", key, fields, tags)
|
||||
|
|
@ -79,11 +83,15 @@ func (m *Influx) writeComplexPoint(writer pointWriter, key string, val any, tags
|
|||
|
||||
for i := range typ.NumField() {
|
||||
if f := typ.Field(i); f.IsExported() {
|
||||
if val.Field(i).IsZero() && omitEmpty(f) {
|
||||
if val.Field(i).IsZero() && jsonOmitEmpty(f) {
|
||||
continue
|
||||
}
|
||||
|
||||
key := key + strings.ToUpper(f.Name[:1]) + f.Name[1:]
|
||||
if tag := influxTagValue(f); tag != "" {
|
||||
key = tag
|
||||
}
|
||||
|
||||
val := val.Field(i).Interface()
|
||||
|
||||
m.writeComplexPoint(writer, key, val, tags)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/evcc-io/evcc/core/types"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
inf2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api/write"
|
||||
|
|
@ -88,7 +89,7 @@ func (w *influxSuite) TestSlice() {
|
|||
}
|
||||
|
||||
func (w *influxSuite) TestMeasurement() {
|
||||
w.WriteParam(util.Param{Key: "battery", Val: measurement{Power: 1, Soc: lo.ToPtr(10.0)}})
|
||||
w.WriteParam(util.Param{Key: "battery", Val: types.Measurement{Power: 1, Soc: lo.ToPtr(10.0)}})
|
||||
w.Equal([]*write.Point{
|
||||
inf2.NewPoint("batteryPower", nil, map[string]any{"value": 1.0}, w.clock.Now()),
|
||||
inf2.NewPoint("batterySoc", nil, map[string]any{"value": 10.0}, w.clock.Now()),
|
||||
|
|
@ -96,7 +97,7 @@ func (w *influxSuite) TestMeasurement() {
|
|||
}
|
||||
|
||||
func (w *influxSuite) TestSliceOfStruct() {
|
||||
w.WriteParam(util.Param{Key: "grid", Val: []measurement{
|
||||
w.WriteParam(util.Param{Key: "grid", Val: []types.Measurement{
|
||||
{Power: 1, Soc: lo.ToPtr(10.0)},
|
||||
{Power: 2, Soc: lo.ToPtr(20.0)},
|
||||
}})
|
||||
|
|
@ -107,3 +108,20 @@ func (w *influxSuite) TestSliceOfStruct() {
|
|||
inf2.NewPoint("gridSoc", map[string]string{"id": "2"}, map[string]any{"value": 20.0}, w.clock.Now()),
|
||||
}, w.p)
|
||||
}
|
||||
|
||||
func (w *influxSuite) TestBatteryState() {
|
||||
w.WriteParam(util.Param{Key: "battery", Val: types.BatteryState{
|
||||
Power: 2,
|
||||
Soc: 20.0,
|
||||
Devices: []types.Measurement{{
|
||||
Power: 1,
|
||||
Soc: lo.ToPtr(10.0),
|
||||
}},
|
||||
}})
|
||||
w.Equal([]*write.Point{
|
||||
inf2.NewPoint("batteryPower", map[string]string{}, map[string]any{"value": 2.0}, w.clock.Now()),
|
||||
inf2.NewPoint("batterySoc", map[string]string{}, map[string]any{"value": 20.0}, w.clock.Now()),
|
||||
inf2.NewPoint("batteryPower", map[string]string{"id": "1"}, map[string]any{"value": 1.0}, w.clock.Now()),
|
||||
inf2.NewPoint("batterySoc", map[string]string{"id": "1"}, map[string]any{"value": 10.0}, w.clock.Now()),
|
||||
}, w.p)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@ func (m *MQTT) encode(v any) string {
|
|||
}
|
||||
}
|
||||
|
||||
func mqttTagAttribute(attr string, f reflect.StructField) bool {
|
||||
return tagAttribute("mqtt", attr, f)
|
||||
}
|
||||
|
||||
func (m *MQTT) publishComplex(topic string, retained bool, payload any) {
|
||||
if _, ok := payload.(fmt.Stringer); ok || payload == nil {
|
||||
m.publishSingleValue(topic, retained, payload)
|
||||
|
|
@ -129,9 +133,14 @@ func (m *MQTT) publishComplex(topic string, retained bool, payload any) {
|
|||
// loop struct
|
||||
for i := range typ.NumField() {
|
||||
if f := typ.Field(i); f.IsExported() {
|
||||
topic := fmt.Sprintf("%s/%s", topic, strings.ToLower(f.Name[:1])+f.Name[1:])
|
||||
topic := topic
|
||||
if !mqttTagAttribute("squash", f) {
|
||||
topic = fmt.Sprintf("%s/%s", topic, strings.ToLower(f.Name[:1])+f.Name[1:])
|
||||
} else {
|
||||
println(1)
|
||||
}
|
||||
|
||||
if val.Field(i).IsZero() && omitEmpty(f) {
|
||||
if val.Field(i).IsZero() && jsonOmitEmpty(f) {
|
||||
m.publishSingleValue(topic, retained, nil)
|
||||
} else {
|
||||
m.publishComplex(topic, retained, val.Field(i).Interface())
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/core/types"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
|
@ -18,14 +19,6 @@ func TestMqttNaNInf(t *testing.T) {
|
|||
assert.Equal(t, "+Inf", m.encode(math.Inf(0)), "Inf not encoded as string")
|
||||
}
|
||||
|
||||
type measurement struct {
|
||||
Power float64 `json:"power"`
|
||||
Energy float64 `json:"energy,omitempty"`
|
||||
Currents []float64 `json:"currents,omitempty"`
|
||||
Soc *float64 `json:"soc,omitempty"`
|
||||
Controllable *bool `json:"controllable,omitempty"`
|
||||
}
|
||||
|
||||
func TestMqttTypes(t *testing.T) {
|
||||
suite.Run(t, new(mqttSuite))
|
||||
}
|
||||
|
|
@ -104,22 +97,41 @@ func (suite *mqttSuite) TestSlice() {
|
|||
suite.Equal([]string{"2", "10", "20"}, suite.payloads, "payloads")
|
||||
}
|
||||
|
||||
func (suite *mqttSuite) TestGrid() {
|
||||
topics := []string{"test/power", "test/energy", "test/currents", "test/soc", "test/controllable"}
|
||||
func (suite *mqttSuite) TestMeasurement() {
|
||||
topics := []string{"test/title", "test/icon", "test/power", "test/energy", "test/powers", "test/currents", "test/excessDCPower", "test/capacity", "test/soc", "test/controllable"}
|
||||
|
||||
suite.publish("test", false, measurement{})
|
||||
suite.publish("test", false, types.Measurement{})
|
||||
suite.Equal(topics, suite.topics, "topics")
|
||||
suite.Equal([]string{"0", "", "", "", ""}, suite.payloads, "payloads")
|
||||
suite.Equal([]string{"", "", "0", "", "", "", "", "", "", ""}, suite.payloads, "payloads")
|
||||
|
||||
suite.publish("test", false, measurement{Energy: 1})
|
||||
suite.publish("test", false, types.Measurement{Energy: 1})
|
||||
suite.Equal(topics, suite.topics, "topics")
|
||||
suite.Equal([]string{"0", "1", "", "", ""}, suite.payloads, "payloads")
|
||||
suite.Equal([]string{"", "", "0", "1", "", "", "", "", "", ""}, suite.payloads, "payloads")
|
||||
|
||||
suite.publish("test", false, measurement{Controllable: lo.ToPtr(false)})
|
||||
suite.publish("test", false, types.Measurement{Controllable: lo.ToPtr(false)})
|
||||
suite.Equal(topics, suite.topics, "topics")
|
||||
suite.Equal([]string{"0", "", "", "", "false"}, suite.payloads, "payloads")
|
||||
suite.Equal([]string{"", "", "0", "", "", "", "", "", "", "false"}, suite.payloads, "payloads")
|
||||
|
||||
suite.publish("test", false, measurement{Currents: []float64{1, 2, 3}})
|
||||
suite.publish("test", false, types.Measurement{Currents: []float64{1, 2, 3}})
|
||||
suite.Equal(append(topics, "test/currents/1", "test/currents/2", "test/currents/3"), suite.topics, "topics")
|
||||
suite.Equal([]string{"0", "", "3", "", "", "1", "2", "3"}, suite.payloads, "payloads")
|
||||
suite.Equal([]string{"", "", "0", "", "", "3", "", "", "", "", "1", "2", "3"}, suite.payloads, "payloads")
|
||||
}
|
||||
|
||||
func (suite *mqttSuite) TestBatteryState() {
|
||||
topics := []string{
|
||||
"test/power", "test/energy", "test/capacity", "test/soc", "test/devices",
|
||||
"test/devices/1/title", "test/devices/1/icon", "test/devices/1/power", "test/devices/1/energy", "test/devices/1/powers", "test/devices/1/currents", "test/devices/1/excessDCPower", "test/devices/1/capacity", "test/devices/1/soc", "test/devices/1/controllable",
|
||||
}
|
||||
|
||||
suite.publish("test", false, types.BatteryState{
|
||||
Power: 2,
|
||||
Soc: 20.0,
|
||||
Devices: []types.Measurement{{
|
||||
Power: 1,
|
||||
Soc: lo.ToPtr(10.0),
|
||||
}},
|
||||
})
|
||||
|
||||
suite.Equal(topics, suite.topics, "topics")
|
||||
suite.Equal([]string{"2", "", "", "20", "1", "", "", "1", "", "", "", "", "", "10", ""}, suite.payloads, "payloads")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue