Skip to content

Commit b02a403

Browse files
authored
Heating: add time-based energy statistics (today, 24h, 7d) to loadpoint tile (#31781)
1 parent 31cf974 commit b02a403

15 files changed

Lines changed: 236 additions & 26 deletions

File tree

assets/js/components/Loadpoints/Loadpoint.stories.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,7 @@ Heating.args = {
192192
vehicleSoc: 48,
193193
effectiveLimitSoc: 60,
194194
vehicleLimitSoc: 65,
195+
todayEnergy: 3200,
196+
last24hEnergy: 4100,
197+
last7dEnergy: 26500,
195198
};

assets/js/components/Loadpoints/Loadpoint.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@
7171
/>
7272
</div>
7373
<LabelAndValue
74+
v-if="integratedDevice"
75+
:label="$t('main.loadpoint.todayEnergy')"
76+
:value="todayEnergy"
77+
:valueFmt="fmtEnergy"
78+
align="center"
79+
/>
80+
<LabelAndValue
81+
v-else
7482
v-show="socBasedCharging"
7583
:label="$t('main.loadpoint.charged')"
7684
:value="chargedEnergy"
@@ -205,6 +213,9 @@ export default defineComponent({
205213
vehicleWelcomeActive: Boolean,
206214
chargePower: { type: Number, default: 0 },
207215
chargedEnergy: { type: Number, default: 0 },
216+
todayEnergy: { type: Number, default: 0 },
217+
last24hEnergy: Number,
218+
last7dEnergy: Number,
208219
chargeRemainingDuration: { type: Number, default: 0 },
209220
210221
// other information

assets/js/components/Loadpoints/SessionInfo.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import { defineComponent, type PropType } from "vue";
3131
import LabelAndValue from "../Helper/LabelAndValue.vue";
3232
import CustomSelect from "../Helper/CustomSelect.vue";
33-
import formatter from "@/mixins/formatter";
33+
import formatter, { POWER_UNIT } from "@/mixins/formatter";
3434
import { getLoadpointSessionInfo, setLoadpointSessionInfo } from "@/uiLoadpoints";
3535
import type { CURRENCY, SelectOption, SessionInfoKey } from "@/types/evcc";
3636
@@ -51,6 +51,8 @@ export default defineComponent({
5151
chargeRemainingDurationInterpolated: { type: Number, default: 0 },
5252
chargeDurationInterpolated: Number,
5353
sessionEnergy: { type: Number, default: 0 },
54+
last24hEnergy: Number,
55+
last7dEnergy: Number,
5456
tariffCo2: Number,
5557
tariffGrid: Number,
5658
},
@@ -107,6 +109,16 @@ export default defineComponent({
107109
value: this.emissionFormatted,
108110
available: this.tariffCo2 !== undefined,
109111
},
112+
{
113+
key: "last24hEnergy" as const,
114+
value: this.fmtWh(this.last24hEnergy ?? 0, POWER_UNIT.AUTO),
115+
available: this.last24hEnergy !== undefined,
116+
},
117+
{
118+
key: "last7dEnergy" as const,
119+
value: this.fmtWh(this.last7dEnergy ?? 0, POWER_UNIT.AUTO),
120+
available: this.last7dEnergy !== undefined,
121+
},
110122
];
111123
// only show options that are available
112124
return result.filter(({ available }) => available === undefined || available);

assets/js/types/evcc.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,8 @@ export interface Loadpoint {
360360
priority: number;
361361
pvAction: PV_ACTION;
362362
pvRemaining: number;
363+
last24hEnergy?: number;
364+
last7dEnergy?: number;
363365
sessionCo2PerKWh: number | null;
364366
sessionEnergy: number;
365367
sessionPrice: number | null;
@@ -373,6 +375,7 @@ export interface Loadpoint {
373375
smartFeedInPriorityNextStart: string | null;
374376
suggestion?: LoadpointSuggestion | null;
375377
title: string;
378+
todayEnergy?: number;
376379
vehicleClimaterActive: boolean | null;
377380
vehicleDetectionActive: boolean;
378381
vehicleLimitSoc: number;
@@ -503,7 +506,9 @@ export type SessionInfoKey =
503506
| "avgPrice"
504507
| "price"
505508
| "emission"
506-
| "co2";
509+
| "co2"
510+
| "last24hEnergy"
511+
| "last7dEnergy";
507512

508513
export interface SponsorStatus {
509514
name?: string;

cmd/demo.yaml

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ javascript:
1717
loadpoints: [
1818
{ enabled: true, vehicleSoc: 62, maxcurrent: 6, phases: 1, chargepower: 0 },
1919
{ enabled: false, vehicleSoc: 22, maxcurrent: 0, phases: 3, chargepower: 0 }
20-
]
20+
],
21+
heatpump: { mode: 2, power: 0, temp: 48 }
2122
};
2223
function logState() {
2324
console.log("state:", JSON.stringify(state));
@@ -30,7 +31,7 @@ meters:
3031
source: js
3132
vm: shared
3233
script: |
33-
state.gridpower = state.loadpoints[0].chargepower + state.loadpoints[1].chargepower + state.residualpower - batterypower - pvpower;
34+
state.gridpower = state.loadpoints[0].chargepower + state.loadpoints[1].chargepower + state.heatpump.power + state.residualpower - batterypower - pvpower;
3435
state.gridpower;
3536
in:
3637
- name: pvpower
@@ -167,9 +168,42 @@ chargers:
167168
if (phases === 1) lp.phases = 1; else lp.phases = 3;
168169
lp.phases;
169170
171+
- name: charger_3
172+
type: sgready
173+
features: [heating, integrateddevice, continuous, switchdevice]
174+
setmode:
175+
source: js
176+
vm: shared
177+
script: |
178+
state.heatpump.mode = mode;
179+
mode;
180+
getmode:
181+
source: js
182+
vm: shared
183+
script: state.heatpump.mode;
184+
power:
185+
source: js
186+
vm: shared
187+
script: |
188+
var hp = state.heatpump;
189+
hp.power = hp.mode === 3 ? 3800 + 400 * Math.random() : 300 + 300 * Math.random();
190+
hp.power;
191+
temp:
192+
source: js
193+
vm: shared
194+
script: |
195+
var hp = state.heatpump;
196+
if (hp.mode === 3) hp.temp += 0.2; else hp.temp -= 0.1;
197+
if (hp.temp > 65) hp.temp = 65;
198+
if (hp.temp < 35) hp.temp = 35;
199+
hp.temp;
200+
limittemp:
201+
source: const
202+
value: 65
203+
170204
vehicles:
171205
- name: vehicle_1
172-
title: blauer e-Golf
206+
title: blue e-Golf
173207
type: custom
174208
soc:
175209
source: js
@@ -189,7 +223,7 @@ vehicles:
189223
range
190224
capacity: 44
191225
- name: vehicle_2
192-
title: weißes Model 3
226+
title: white Model 3
193227
type: custom
194228
soc:
195229
source: js
@@ -219,19 +253,14 @@ vehicles:
219253
- name: vehicle_3
220254
type: template
221255
template: offline
222-
title: grüner Honda e
256+
title: green Honda e
223257
capacity: 8
224258
- name: vehicle_4
225259
type: template
226260
template: offline
227-
title: schwarzes VanMoof
261+
title: black VanMoof
228262
icon: bike
229263
capacity: 0.46
230-
- name: vehicle_5
231-
type: template
232-
template: offline
233-
title: Wärmepumpe
234-
icon: waterheater
235264

236265
site:
237266
title: Demo Mode
@@ -251,6 +280,12 @@ loadpoints:
251280
mode: "off"
252281
meter: meter_charger_2
253282
vehicle: vehicle_2
283+
- title: Heat pump
284+
charger: charger_3
285+
mode: pv
286+
ui:
287+
minTemp: 30
288+
maxTemp: 70
254289

255290
tariffs:
256291
currency: EUR # three letter ISO-4217 currency code (default EUR)

core/keys/loadpoint.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ const (
7676
ChargedEnergy = "chargedEnergy" // charged energy
7777
ChargeDuration = "chargeDuration" // charge duration
7878
ChargeTotalImport = "chargeTotalImport" // charge meter total import
79+
TodayEnergy = "todayEnergy" // energy since midnight
80+
Last24hEnergy = "last24hEnergy" // energy, rolling 24h
81+
Last7dEnergy = "last7dEnergy" // energy, rolling 7 days
7982

8083
// session
8184
ConnectedDuration = "connectedDuration" // connected duration

core/loadpoint.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ type Loadpoint struct {
9797

9898
Soc loadpoint.SocConfig
9999
Enable, Disable loadpoint.ThresholdConfig
100-
ui loadpoint.UIConfig // display-only, not used in control logic
100+
Ui loadpoint.UIConfig // display-only, not used in control logic
101101

102102
// from yaml
103103
DefaultMode api.ChargeMode `mapstructure:"mode"` // Default charge mode, used for disconnect
@@ -389,7 +389,7 @@ func (lp *Loadpoint) restoreSettings() {
389389

390390
var ui loadpoint.UIConfig
391391
if err := lp.settings.Json(keys.UI, &ui); err == nil {
392-
lp.ui = ui
392+
lp.Ui = ui
393393
}
394394

395395
t, err1 := lp.settings.Time(keys.PlanTime)
@@ -705,7 +705,7 @@ func (lp *Loadpoint) Prepare(site site.API, uiChan chan<- util.Param, pushChan c
705705
lp.publish(keys.EnableDelay, lp.Enable.Delay)
706706
lp.publish(keys.DisableDelay, lp.Disable.Delay)
707707

708-
lp.publish(keys.UI, lp.ui)
708+
lp.publish(keys.UI, lp.Ui)
709709

710710
if phases := lp.getChargerPhysicalPhases(); phases != 0 {
711711
if lp.phasesConfigured != phases && lp.phasesConfigured != 0 {
@@ -723,6 +723,7 @@ func (lp *Loadpoint) Prepare(site site.API, uiChan chan<- util.Param, pushChan c
723723
lp.publish(keys.SmartFeedInPriorityLimit, lp.smartFeedInPriorityLimit)
724724
lp.publishTimer(phaseTimer, 0, timerInactive)
725725
lp.publishTimer(pvTimer, 0, timerInactive)
726+
lp.publishEnergyStats()
726727

727728
// charger features
728729
for _, f := range api.FeatureValues() {
@@ -1851,6 +1852,25 @@ func (lp *Loadpoint) publishChargeProgress() {
18511852
lp.chargeEnergy.SetSocTemp(v, lp.chargerHasFeature(api.Heating))
18521853
}
18531854
}
1855+
1856+
lp.publishEnergyStats()
1857+
}
1858+
1859+
// publishEnergyStats publishes time-based energy statistics in Wh
1860+
func (lp *Loadpoint) publishEnergyStats() {
1861+
if lp.chargeEnergy == nil {
1862+
return
1863+
}
1864+
1865+
stats, err := lp.chargeEnergy.EnergyStats()
1866+
if err != nil {
1867+
lp.log.ERROR.Printf("energy stats: %v", err)
1868+
return
1869+
}
1870+
1871+
lp.publish(keys.TodayEnergy, math.Round(stats.Today*1e3))
1872+
lp.publish(keys.Last24hEnergy, math.Round(stats.Last24h*1e3))
1873+
lp.publish(keys.Last7dEnergy, math.Round(stats.Last7d*1e3))
18541874
}
18551875

18561876
// publish state of charge, remaining charge duration and range

core/loadpoint_api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,11 +498,11 @@ func (lp *Loadpoint) SetSocConfig(soc loadpoint.SocConfig) {
498498
func (lp *Loadpoint) GetUI() loadpoint.UIConfig {
499499
lp.RLock()
500500
defer lp.RUnlock()
501-
return lp.ui
501+
return lp.Ui
502502
}
503503

504504
func (lp *Loadpoint) setUI(ui loadpoint.UIConfig) {
505-
lp.ui = ui
505+
lp.Ui = ui
506506
lp.publish(keys.UI, ui)
507507
lp.settings.SetJson(keys.UI, ui)
508508
}

core/metrics/collector.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ const (
2121
)
2222

2323
type Collector struct {
24-
entity entity
25-
accu *Accumulator
26-
started time.Time
24+
entity entity
25+
accu *Accumulator
26+
started time.Time
27+
statsCache EnergyStats
2728
}
2829

2930
func NewCollector(group, name, title string, opt ...func(*Accumulator)) (*Collector, error) {

core/metrics/stats.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package metrics
2+
3+
import (
4+
"time"
5+
6+
"github.com/evcc-io/evcc/server/db"
7+
"github.com/evcc-io/evcc/tariff"
8+
"github.com/jinzhu/now"
9+
)
10+
11+
// EnergyStats holds time-based energy sums in kWh
12+
type EnergyStats struct {
13+
slot time.Time // cache key
14+
Today float64
15+
Last24h float64
16+
Last7d float64
17+
}
18+
19+
// energyStats sums persisted slot energy for the periods ending at slotStart
20+
func energyStats(entity entity, slotStart, midnight time.Time) (EnergyStats, error) {
21+
res := EnergyStats{slot: slotStart}
22+
23+
sqlDB, err := db.Instance.DB()
24+
if err != nil {
25+
return res, err
26+
}
27+
28+
err = sqlDB.QueryRow(`SELECT
29+
COALESCE(SUM(CASE WHEN ts >= ? THEN energy END), 0),
30+
COALESCE(SUM(CASE WHEN ts >= ? THEN energy END), 0),
31+
COALESCE(SUM(energy), 0)
32+
FROM meters WHERE meter = ? AND ts >= ? AND ts < ?`,
33+
midnight.Unix(), slotStart.Add(-24*time.Hour).Unix(),
34+
entity.Id, slotStart.AddDate(0, 0, -7).Unix(), slotStart.Unix(),
35+
).Scan(&res.Today, &res.Last24h, &res.Last7d)
36+
37+
return res, err
38+
}
39+
40+
// EnergyStats returns time-based energy sums. Persisted sums are cached per
41+
// slot, the in-flight accumulator is added to today only.
42+
func (c *Collector) EnergyStats() (EnergyStats, error) {
43+
slotStart := c.accu.clock.Now().Truncate(tariff.SlotDuration)
44+
45+
if !slotStart.Equal(c.statsCache.slot) {
46+
stats, err := energyStats(c.entity, slotStart, now.With(c.accu.clock.Now()).BeginningOfDay())
47+
if err != nil {
48+
return EnergyStats{}, err
49+
}
50+
51+
c.statsCache = stats
52+
}
53+
54+
res := c.statsCache
55+
res.Today += c.accu.Energy
56+
return res, nil
57+
}

0 commit comments

Comments
 (0)