Heating: add time-based energy statistics (today, 24h, 7d) to loadpoint tile (#31781)
This commit is contained in:
parent
31cf9748aa
commit
b02a40353f
15 changed files with 236 additions and 26 deletions
|
|
@ -192,4 +192,7 @@ Heating.args = {
|
||||||
vehicleSoc: 48,
|
vehicleSoc: 48,
|
||||||
effectiveLimitSoc: 60,
|
effectiveLimitSoc: 60,
|
||||||
vehicleLimitSoc: 65,
|
vehicleLimitSoc: 65,
|
||||||
|
todayEnergy: 3200,
|
||||||
|
last24hEnergy: 4100,
|
||||||
|
last7dEnergy: 26500,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,14 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<LabelAndValue
|
<LabelAndValue
|
||||||
|
v-if="integratedDevice"
|
||||||
|
:label="$t('main.loadpoint.todayEnergy')"
|
||||||
|
:value="todayEnergy"
|
||||||
|
:valueFmt="fmtEnergy"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<LabelAndValue
|
||||||
|
v-else
|
||||||
v-show="socBasedCharging"
|
v-show="socBasedCharging"
|
||||||
:label="$t('main.loadpoint.charged')"
|
:label="$t('main.loadpoint.charged')"
|
||||||
:value="chargedEnergy"
|
:value="chargedEnergy"
|
||||||
|
|
@ -205,6 +213,9 @@ export default defineComponent({
|
||||||
vehicleWelcomeActive: Boolean,
|
vehicleWelcomeActive: Boolean,
|
||||||
chargePower: { type: Number, default: 0 },
|
chargePower: { type: Number, default: 0 },
|
||||||
chargedEnergy: { type: Number, default: 0 },
|
chargedEnergy: { type: Number, default: 0 },
|
||||||
|
todayEnergy: { type: Number, default: 0 },
|
||||||
|
last24hEnergy: Number,
|
||||||
|
last7dEnergy: Number,
|
||||||
chargeRemainingDuration: { type: Number, default: 0 },
|
chargeRemainingDuration: { type: Number, default: 0 },
|
||||||
|
|
||||||
// other information
|
// other information
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import LabelAndValue from "../Helper/LabelAndValue.vue";
|
import LabelAndValue from "../Helper/LabelAndValue.vue";
|
||||||
import CustomSelect from "../Helper/CustomSelect.vue";
|
import CustomSelect from "../Helper/CustomSelect.vue";
|
||||||
import formatter from "@/mixins/formatter";
|
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
||||||
import { getLoadpointSessionInfo, setLoadpointSessionInfo } from "@/uiLoadpoints";
|
import { getLoadpointSessionInfo, setLoadpointSessionInfo } from "@/uiLoadpoints";
|
||||||
import type { CURRENCY, SelectOption, SessionInfoKey } from "@/types/evcc";
|
import type { CURRENCY, SelectOption, SessionInfoKey } from "@/types/evcc";
|
||||||
|
|
||||||
|
|
@ -51,6 +51,8 @@ export default defineComponent({
|
||||||
chargeRemainingDurationInterpolated: { type: Number, default: 0 },
|
chargeRemainingDurationInterpolated: { type: Number, default: 0 },
|
||||||
chargeDurationInterpolated: Number,
|
chargeDurationInterpolated: Number,
|
||||||
sessionEnergy: { type: Number, default: 0 },
|
sessionEnergy: { type: Number, default: 0 },
|
||||||
|
last24hEnergy: Number,
|
||||||
|
last7dEnergy: Number,
|
||||||
tariffCo2: Number,
|
tariffCo2: Number,
|
||||||
tariffGrid: Number,
|
tariffGrid: Number,
|
||||||
},
|
},
|
||||||
|
|
@ -107,6 +109,16 @@ export default defineComponent({
|
||||||
value: this.emissionFormatted,
|
value: this.emissionFormatted,
|
||||||
available: this.tariffCo2 !== undefined,
|
available: this.tariffCo2 !== undefined,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "last24hEnergy" as const,
|
||||||
|
value: this.fmtWh(this.last24hEnergy ?? 0, POWER_UNIT.AUTO),
|
||||||
|
available: this.last24hEnergy !== undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "last7dEnergy" as const,
|
||||||
|
value: this.fmtWh(this.last7dEnergy ?? 0, POWER_UNIT.AUTO),
|
||||||
|
available: this.last7dEnergy !== undefined,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
// only show options that are available
|
// only show options that are available
|
||||||
return result.filter(({ available }) => available === undefined || available);
|
return result.filter(({ available }) => available === undefined || available);
|
||||||
|
|
|
||||||
|
|
@ -360,6 +360,8 @@ export interface Loadpoint {
|
||||||
priority: number;
|
priority: number;
|
||||||
pvAction: PV_ACTION;
|
pvAction: PV_ACTION;
|
||||||
pvRemaining: number;
|
pvRemaining: number;
|
||||||
|
last24hEnergy?: number;
|
||||||
|
last7dEnergy?: number;
|
||||||
sessionCo2PerKWh: number | null;
|
sessionCo2PerKWh: number | null;
|
||||||
sessionEnergy: number;
|
sessionEnergy: number;
|
||||||
sessionPrice: number | null;
|
sessionPrice: number | null;
|
||||||
|
|
@ -373,6 +375,7 @@ export interface Loadpoint {
|
||||||
smartFeedInPriorityNextStart: string | null;
|
smartFeedInPriorityNextStart: string | null;
|
||||||
suggestion?: LoadpointSuggestion | null;
|
suggestion?: LoadpointSuggestion | null;
|
||||||
title: string;
|
title: string;
|
||||||
|
todayEnergy?: number;
|
||||||
vehicleClimaterActive: boolean | null;
|
vehicleClimaterActive: boolean | null;
|
||||||
vehicleDetectionActive: boolean;
|
vehicleDetectionActive: boolean;
|
||||||
vehicleLimitSoc: number;
|
vehicleLimitSoc: number;
|
||||||
|
|
@ -503,7 +506,9 @@ export type SessionInfoKey =
|
||||||
| "avgPrice"
|
| "avgPrice"
|
||||||
| "price"
|
| "price"
|
||||||
| "emission"
|
| "emission"
|
||||||
| "co2";
|
| "co2"
|
||||||
|
| "last24hEnergy"
|
||||||
|
| "last7dEnergy";
|
||||||
|
|
||||||
export interface SponsorStatus {
|
export interface SponsorStatus {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ javascript:
|
||||||
loadpoints: [
|
loadpoints: [
|
||||||
{ enabled: true, vehicleSoc: 62, maxcurrent: 6, phases: 1, chargepower: 0 },
|
{ enabled: true, vehicleSoc: 62, maxcurrent: 6, phases: 1, chargepower: 0 },
|
||||||
{ enabled: false, vehicleSoc: 22, maxcurrent: 0, phases: 3, chargepower: 0 }
|
{ enabled: false, vehicleSoc: 22, maxcurrent: 0, phases: 3, chargepower: 0 }
|
||||||
]
|
],
|
||||||
|
heatpump: { mode: 2, power: 0, temp: 48 }
|
||||||
};
|
};
|
||||||
function logState() {
|
function logState() {
|
||||||
console.log("state:", JSON.stringify(state));
|
console.log("state:", JSON.stringify(state));
|
||||||
|
|
@ -30,7 +31,7 @@ meters:
|
||||||
source: js
|
source: js
|
||||||
vm: shared
|
vm: shared
|
||||||
script: |
|
script: |
|
||||||
state.gridpower = state.loadpoints[0].chargepower + state.loadpoints[1].chargepower + state.residualpower - batterypower - pvpower;
|
state.gridpower = state.loadpoints[0].chargepower + state.loadpoints[1].chargepower + state.heatpump.power + state.residualpower - batterypower - pvpower;
|
||||||
state.gridpower;
|
state.gridpower;
|
||||||
in:
|
in:
|
||||||
- name: pvpower
|
- name: pvpower
|
||||||
|
|
@ -167,9 +168,42 @@ chargers:
|
||||||
if (phases === 1) lp.phases = 1; else lp.phases = 3;
|
if (phases === 1) lp.phases = 1; else lp.phases = 3;
|
||||||
lp.phases;
|
lp.phases;
|
||||||
|
|
||||||
|
- name: charger_3
|
||||||
|
type: sgready
|
||||||
|
features: [heating, integrateddevice, continuous, switchdevice]
|
||||||
|
setmode:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: |
|
||||||
|
state.heatpump.mode = mode;
|
||||||
|
mode;
|
||||||
|
getmode:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: state.heatpump.mode;
|
||||||
|
power:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: |
|
||||||
|
var hp = state.heatpump;
|
||||||
|
hp.power = hp.mode === 3 ? 3800 + 400 * Math.random() : 300 + 300 * Math.random();
|
||||||
|
hp.power;
|
||||||
|
temp:
|
||||||
|
source: js
|
||||||
|
vm: shared
|
||||||
|
script: |
|
||||||
|
var hp = state.heatpump;
|
||||||
|
if (hp.mode === 3) hp.temp += 0.2; else hp.temp -= 0.1;
|
||||||
|
if (hp.temp > 65) hp.temp = 65;
|
||||||
|
if (hp.temp < 35) hp.temp = 35;
|
||||||
|
hp.temp;
|
||||||
|
limittemp:
|
||||||
|
source: const
|
||||||
|
value: 65
|
||||||
|
|
||||||
vehicles:
|
vehicles:
|
||||||
- name: vehicle_1
|
- name: vehicle_1
|
||||||
title: blauer e-Golf
|
title: blue e-Golf
|
||||||
type: custom
|
type: custom
|
||||||
soc:
|
soc:
|
||||||
source: js
|
source: js
|
||||||
|
|
@ -189,7 +223,7 @@ vehicles:
|
||||||
range
|
range
|
||||||
capacity: 44
|
capacity: 44
|
||||||
- name: vehicle_2
|
- name: vehicle_2
|
||||||
title: weißes Model 3
|
title: white Model 3
|
||||||
type: custom
|
type: custom
|
||||||
soc:
|
soc:
|
||||||
source: js
|
source: js
|
||||||
|
|
@ -219,19 +253,14 @@ vehicles:
|
||||||
- name: vehicle_3
|
- name: vehicle_3
|
||||||
type: template
|
type: template
|
||||||
template: offline
|
template: offline
|
||||||
title: grüner Honda e
|
title: green Honda e
|
||||||
capacity: 8
|
capacity: 8
|
||||||
- name: vehicle_4
|
- name: vehicle_4
|
||||||
type: template
|
type: template
|
||||||
template: offline
|
template: offline
|
||||||
title: schwarzes VanMoof
|
title: black VanMoof
|
||||||
icon: bike
|
icon: bike
|
||||||
capacity: 0.46
|
capacity: 0.46
|
||||||
- name: vehicle_5
|
|
||||||
type: template
|
|
||||||
template: offline
|
|
||||||
title: Wärmepumpe
|
|
||||||
icon: waterheater
|
|
||||||
|
|
||||||
site:
|
site:
|
||||||
title: Demo Mode
|
title: Demo Mode
|
||||||
|
|
@ -251,6 +280,12 @@ loadpoints:
|
||||||
mode: "off"
|
mode: "off"
|
||||||
meter: meter_charger_2
|
meter: meter_charger_2
|
||||||
vehicle: vehicle_2
|
vehicle: vehicle_2
|
||||||
|
- title: Heat pump
|
||||||
|
charger: charger_3
|
||||||
|
mode: pv
|
||||||
|
ui:
|
||||||
|
minTemp: 30
|
||||||
|
maxTemp: 70
|
||||||
|
|
||||||
tariffs:
|
tariffs:
|
||||||
currency: EUR # three letter ISO-4217 currency code (default EUR)
|
currency: EUR # three letter ISO-4217 currency code (default EUR)
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,9 @@ const (
|
||||||
ChargedEnergy = "chargedEnergy" // charged energy
|
ChargedEnergy = "chargedEnergy" // charged energy
|
||||||
ChargeDuration = "chargeDuration" // charge duration
|
ChargeDuration = "chargeDuration" // charge duration
|
||||||
ChargeTotalImport = "chargeTotalImport" // charge meter total import
|
ChargeTotalImport = "chargeTotalImport" // charge meter total import
|
||||||
|
TodayEnergy = "todayEnergy" // energy since midnight
|
||||||
|
Last24hEnergy = "last24hEnergy" // energy, rolling 24h
|
||||||
|
Last7dEnergy = "last7dEnergy" // energy, rolling 7 days
|
||||||
|
|
||||||
// session
|
// session
|
||||||
ConnectedDuration = "connectedDuration" // connected duration
|
ConnectedDuration = "connectedDuration" // connected duration
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ type Loadpoint struct {
|
||||||
|
|
||||||
Soc loadpoint.SocConfig
|
Soc loadpoint.SocConfig
|
||||||
Enable, Disable loadpoint.ThresholdConfig
|
Enable, Disable loadpoint.ThresholdConfig
|
||||||
ui loadpoint.UIConfig // display-only, not used in control logic
|
Ui loadpoint.UIConfig // display-only, not used in control logic
|
||||||
|
|
||||||
// from yaml
|
// from yaml
|
||||||
DefaultMode api.ChargeMode `mapstructure:"mode"` // Default charge mode, used for disconnect
|
DefaultMode api.ChargeMode `mapstructure:"mode"` // Default charge mode, used for disconnect
|
||||||
|
|
@ -389,7 +389,7 @@ func (lp *Loadpoint) restoreSettings() {
|
||||||
|
|
||||||
var ui loadpoint.UIConfig
|
var ui loadpoint.UIConfig
|
||||||
if err := lp.settings.Json(keys.UI, &ui); err == nil {
|
if err := lp.settings.Json(keys.UI, &ui); err == nil {
|
||||||
lp.ui = ui
|
lp.Ui = ui
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err1 := lp.settings.Time(keys.PlanTime)
|
t, err1 := lp.settings.Time(keys.PlanTime)
|
||||||
|
|
@ -705,7 +705,7 @@ func (lp *Loadpoint) Prepare(site site.API, uiChan chan<- util.Param, pushChan c
|
||||||
lp.publish(keys.EnableDelay, lp.Enable.Delay)
|
lp.publish(keys.EnableDelay, lp.Enable.Delay)
|
||||||
lp.publish(keys.DisableDelay, lp.Disable.Delay)
|
lp.publish(keys.DisableDelay, lp.Disable.Delay)
|
||||||
|
|
||||||
lp.publish(keys.UI, lp.ui)
|
lp.publish(keys.UI, lp.Ui)
|
||||||
|
|
||||||
if phases := lp.getChargerPhysicalPhases(); phases != 0 {
|
if phases := lp.getChargerPhysicalPhases(); phases != 0 {
|
||||||
if lp.phasesConfigured != phases && lp.phasesConfigured != 0 {
|
if lp.phasesConfigured != phases && lp.phasesConfigured != 0 {
|
||||||
|
|
@ -723,6 +723,7 @@ func (lp *Loadpoint) Prepare(site site.API, uiChan chan<- util.Param, pushChan c
|
||||||
lp.publish(keys.SmartFeedInPriorityLimit, lp.smartFeedInPriorityLimit)
|
lp.publish(keys.SmartFeedInPriorityLimit, lp.smartFeedInPriorityLimit)
|
||||||
lp.publishTimer(phaseTimer, 0, timerInactive)
|
lp.publishTimer(phaseTimer, 0, timerInactive)
|
||||||
lp.publishTimer(pvTimer, 0, timerInactive)
|
lp.publishTimer(pvTimer, 0, timerInactive)
|
||||||
|
lp.publishEnergyStats()
|
||||||
|
|
||||||
// charger features
|
// charger features
|
||||||
for _, f := range api.FeatureValues() {
|
for _, f := range api.FeatureValues() {
|
||||||
|
|
@ -1851,6 +1852,25 @@ func (lp *Loadpoint) publishChargeProgress() {
|
||||||
lp.chargeEnergy.SetSocTemp(v, lp.chargerHasFeature(api.Heating))
|
lp.chargeEnergy.SetSocTemp(v, lp.chargerHasFeature(api.Heating))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lp.publishEnergyStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// publishEnergyStats publishes time-based energy statistics in Wh
|
||||||
|
func (lp *Loadpoint) publishEnergyStats() {
|
||||||
|
if lp.chargeEnergy == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := lp.chargeEnergy.EnergyStats()
|
||||||
|
if err != nil {
|
||||||
|
lp.log.ERROR.Printf("energy stats: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.publish(keys.TodayEnergy, math.Round(stats.Today*1e3))
|
||||||
|
lp.publish(keys.Last24hEnergy, math.Round(stats.Last24h*1e3))
|
||||||
|
lp.publish(keys.Last7dEnergy, math.Round(stats.Last7d*1e3))
|
||||||
}
|
}
|
||||||
|
|
||||||
// publish state of charge, remaining charge duration and range
|
// publish state of charge, remaining charge duration and range
|
||||||
|
|
|
||||||
|
|
@ -498,11 +498,11 @@ func (lp *Loadpoint) SetSocConfig(soc loadpoint.SocConfig) {
|
||||||
func (lp *Loadpoint) GetUI() loadpoint.UIConfig {
|
func (lp *Loadpoint) GetUI() loadpoint.UIConfig {
|
||||||
lp.RLock()
|
lp.RLock()
|
||||||
defer lp.RUnlock()
|
defer lp.RUnlock()
|
||||||
return lp.ui
|
return lp.Ui
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lp *Loadpoint) setUI(ui loadpoint.UIConfig) {
|
func (lp *Loadpoint) setUI(ui loadpoint.UIConfig) {
|
||||||
lp.ui = ui
|
lp.Ui = ui
|
||||||
lp.publish(keys.UI, ui)
|
lp.publish(keys.UI, ui)
|
||||||
lp.settings.SetJson(keys.UI, ui)
|
lp.settings.SetJson(keys.UI, ui)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,10 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Collector struct {
|
type Collector struct {
|
||||||
entity entity
|
entity entity
|
||||||
accu *Accumulator
|
accu *Accumulator
|
||||||
started time.Time
|
started time.Time
|
||||||
|
statsCache EnergyStats
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCollector(group, name, title string, opt ...func(*Accumulator)) (*Collector, error) {
|
func NewCollector(group, name, title string, opt ...func(*Accumulator)) (*Collector, error) {
|
||||||
|
|
|
||||||
57
core/metrics/stats.go
Normal file
57
core/metrics/stats.go
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/server/db"
|
||||||
|
"github.com/evcc-io/evcc/tariff"
|
||||||
|
"github.com/jinzhu/now"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnergyStats holds time-based energy sums in kWh
|
||||||
|
type EnergyStats struct {
|
||||||
|
slot time.Time // cache key
|
||||||
|
Today float64
|
||||||
|
Last24h float64
|
||||||
|
Last7d float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// energyStats sums persisted slot energy for the periods ending at slotStart
|
||||||
|
func energyStats(entity entity, slotStart, midnight time.Time) (EnergyStats, error) {
|
||||||
|
res := EnergyStats{slot: slotStart}
|
||||||
|
|
||||||
|
sqlDB, err := db.Instance.DB()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sqlDB.QueryRow(`SELECT
|
||||||
|
COALESCE(SUM(CASE WHEN ts >= ? THEN energy END), 0),
|
||||||
|
COALESCE(SUM(CASE WHEN ts >= ? THEN energy END), 0),
|
||||||
|
COALESCE(SUM(energy), 0)
|
||||||
|
FROM meters WHERE meter = ? AND ts >= ? AND ts < ?`,
|
||||||
|
midnight.Unix(), slotStart.Add(-24*time.Hour).Unix(),
|
||||||
|
entity.Id, slotStart.AddDate(0, 0, -7).Unix(), slotStart.Unix(),
|
||||||
|
).Scan(&res.Today, &res.Last24h, &res.Last7d)
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnergyStats returns time-based energy sums. Persisted sums are cached per
|
||||||
|
// slot, the in-flight accumulator is added to today only.
|
||||||
|
func (c *Collector) EnergyStats() (EnergyStats, error) {
|
||||||
|
slotStart := c.accu.clock.Now().Truncate(tariff.SlotDuration)
|
||||||
|
|
||||||
|
if !slotStart.Equal(c.statsCache.slot) {
|
||||||
|
stats, err := energyStats(c.entity, slotStart, now.With(c.accu.clock.Now()).BeginningOfDay())
|
||||||
|
if err != nil {
|
||||||
|
return EnergyStats{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.statsCache = stats
|
||||||
|
}
|
||||||
|
|
||||||
|
res := c.statsCache
|
||||||
|
res.Today += c.accu.Energy
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
56
core/metrics/stats_test.go
Normal file
56
core/metrics/stats_test.go
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/benbjohnson/clock"
|
||||||
|
"github.com/evcc-io/evcc/server/db"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCollectorEnergyStats(t *testing.T) {
|
||||||
|
clock := clock.NewMock()
|
||||||
|
|
||||||
|
loc := time.Now().Location()
|
||||||
|
midnight := time.Date(2026, 4, 15, 0, 0, 0, 0, loc)
|
||||||
|
clock.Set(midnight.Add(2*time.Hour + 5*time.Minute))
|
||||||
|
|
||||||
|
slotStart := clock.Now().Truncate(15 * time.Minute)
|
||||||
|
|
||||||
|
require.NoError(t, db.NewInstance("sqlite", ":memory:"))
|
||||||
|
require.NoError(t, SetupSchema())
|
||||||
|
|
||||||
|
col, err := NewCollector("stats", "stats", "", WithClock(clock))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
e := col.entity
|
||||||
|
require.NoError(t, persist(e, slotStart.AddDate(0, 0, -8), 100, 0, nil)) // outside 7d
|
||||||
|
require.NoError(t, persist(e, slotStart.Add(-24*time.Hour-15*time.Minute), 5, 0, nil)) // 7d only
|
||||||
|
require.NoError(t, persist(e, slotStart.Add(-24*time.Hour), 7, 0, nil)) // 24h window start
|
||||||
|
require.NoError(t, persist(e, slotStart.Add(-12*time.Hour), 3, 0, nil)) // yesterday, within 24h
|
||||||
|
require.NoError(t, persist(e, midnight, 1, 0, nil)) // first slot today
|
||||||
|
require.NoError(t, persist(e, slotStart.Add(-15*time.Minute), 2, 0, nil)) // last completed slot
|
||||||
|
require.NoError(t, persist(e, slotStart, 4, 0, nil)) // current slot, excluded
|
||||||
|
|
||||||
|
stats, err := col.EnergyStats()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.InDelta(t, 3, stats.Today, 1e-9)
|
||||||
|
require.InDelta(t, 13, stats.Last24h, 1e-9)
|
||||||
|
require.InDelta(t, 18, stats.Last7d, 1e-9)
|
||||||
|
|
||||||
|
// in-flight accumulator counts towards today only
|
||||||
|
col.accu.Energy = 0.5
|
||||||
|
stats, err = col.EnergyStats()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.InDelta(t, 3.5, stats.Today, 1e-9)
|
||||||
|
require.InDelta(t, 13, stats.Last24h, 1e-9)
|
||||||
|
|
||||||
|
// next slot: cache refreshes, windows shift by one slot
|
||||||
|
clock.Add(15 * time.Minute)
|
||||||
|
stats, err = col.EnergyStats()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.InDelta(t, 7.5, stats.Today, 1e-9) // 1+2+4 + in-flight
|
||||||
|
require.InDelta(t, 10, stats.Last24h, 1e-9) // drops 7, gains 4
|
||||||
|
require.InDelta(t, 22, stats.Last7d, 1e-9) // gains 4, 8d slot stays out
|
||||||
|
}
|
||||||
|
|
@ -1368,12 +1368,15 @@
|
||||||
"emission": "Emission",
|
"emission": "Emission",
|
||||||
"fallbackName": "Ladepunkt",
|
"fallbackName": "Ladepunkt",
|
||||||
"finished": "Ladeende",
|
"finished": "Ladeende",
|
||||||
|
"last24hEnergy": "Letzte 24h",
|
||||||
|
"last7dEnergy": "Letzte 7 Tage",
|
||||||
"power": "Leistung",
|
"power": "Leistung",
|
||||||
"price": "Kosten",
|
"price": "Kosten",
|
||||||
"remaining": "Restzeit",
|
"remaining": "Restzeit",
|
||||||
"remoteDisabledHard": "{source}: Deaktiviert",
|
"remoteDisabledHard": "{source}: Deaktiviert",
|
||||||
"remoteDisabledSoft": "{source}: Adaptives PV-Laden deaktiviert",
|
"remoteDisabledSoft": "{source}: Adaptives PV-Laden deaktiviert",
|
||||||
"solar": "Sonne"
|
"solar": "Sonne",
|
||||||
|
"todayEnergy": "Heute"
|
||||||
},
|
},
|
||||||
"loadpointSettings": {
|
"loadpointSettings": {
|
||||||
"batteryBoost": {
|
"batteryBoost": {
|
||||||
|
|
|
||||||
|
|
@ -1368,12 +1368,15 @@
|
||||||
"emission": "Emission",
|
"emission": "Emission",
|
||||||
"fallbackName": "Charging point",
|
"fallbackName": "Charging point",
|
||||||
"finished": "Finish time",
|
"finished": "Finish time",
|
||||||
|
"last24hEnergy": "Last 24h",
|
||||||
|
"last7dEnergy": "Last 7 days",
|
||||||
"power": "Power",
|
"power": "Power",
|
||||||
"price": "Cost",
|
"price": "Cost",
|
||||||
"remaining": "Remaining",
|
"remaining": "Remaining",
|
||||||
"remoteDisabledHard": "{source}: turned off",
|
"remoteDisabledHard": "{source}: turned off",
|
||||||
"remoteDisabledSoft": "{source}: turned off adaptive solar-charging",
|
"remoteDisabledSoft": "{source}: turned off adaptive solar-charging",
|
||||||
"solar": "Solar"
|
"solar": "Solar",
|
||||||
|
"todayEnergy": "Today"
|
||||||
},
|
},
|
||||||
"loadpointSettings": {
|
"loadpointSettings": {
|
||||||
"batteryBoost": {
|
"batteryBoost": {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ test.describe("session info", async () => {
|
||||||
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Solar");
|
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Solar");
|
||||||
// by click on value
|
// by click on value
|
||||||
await page.getByTestId("sessionInfoValue").first().click();
|
await page.getByTestId("sessionInfoValue").first().click();
|
||||||
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Duration");
|
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Last 24h");
|
||||||
});
|
});
|
||||||
test("keep selection on reload", async ({ page }) => {
|
test("keep selection on reload", async ({ page }) => {
|
||||||
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Duration");
|
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Duration");
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,11 @@ test.describe("demo mode", async () => {
|
||||||
await expect(page.getByRole("heading", { name: "Demo Mode" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Demo Mode" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("two loadpoints", async ({ page }) => {
|
test("three loadpoints", async ({ page }) => {
|
||||||
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
|
await expect(page.getByTestId("loadpoint")).toHaveCount(3);
|
||||||
await expect(page.getByTestId("loadpoint").nth(0)).toContainText("Carport");
|
await expect(page.getByTestId("loadpoint").nth(0)).toContainText("Carport");
|
||||||
await expect(page.getByTestId("loadpoint").nth(1)).toContainText("Garage");
|
await expect(page.getByTestId("loadpoint").nth(1)).toContainText("Garage");
|
||||||
|
await expect(page.getByTestId("loadpoint").nth(2)).toContainText("Heat pump");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("auth is locked", async ({ page }) => {
|
test("auth is locked", async ({ page }) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue