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