Repository: evcc-io/evcc
Pull request #10306 (2023-10-14 08:20:05)

Add configurable custom tariff
Refs https://github.com/evcc-io/evcc/discussions/8459

@naltatis für die Typen könnte ich mir Folgendes vorstellen:

	TariffTypePriceStatic
	TariffTypePriceDynamic // <- geänderte Bedeutung
	TariffTypePriceForecast // <- neu
	TariffTypeCo2

Dynamic würde damit nur noch bedeuten, dass sich der Preis ändern kann, Forecast dass es einen Ausblick gibt. Würde das fürs UI passen? Co2 wäre aktuell nicht konfigurierbar. Dann müssten wir nochmal in die site schauen.

Beispielkonfiguration:

```
  grid:
    type: custom
    price:
      source: js
      vm: shared
      script: |
        // demo: random prices based on time
        new Date().getSeconds() / 100 + Math.random() / 10;
```

//cc @VolkerK62 

TODO

- [x] ui


Patch:
diff --git a/api/tariff.go b/api/tariff.go
index c433506b..d8b1b37a 100644
--- a/api/tariff.go
+++ b/api/tariff.go
@@ -8,5 +8,6 @@ const (
 	_ TariffType = iota
 	TariffTypePriceStatic
 	TariffTypePriceDynamic
+	TariffTypePriceForecast
 	TariffTypeCo2
 )
diff --git a/api/tarifftype_enumer.go b/api/tarifftype_enumer.go
index ac0a6e74..9a7b11cf 100644
--- a/api/tarifftype_enumer.go
+++ b/api/tarifftype_enumer.go
@@ -7,11 +7,11 @@ import (
 	"strings"
 )
 
-const _TariffTypeName = "pricestaticpricedynamicco2"
+const _TariffTypeName = "pricestaticpricedynamicpriceforecastco2"
 
-var _TariffTypeIndex = [...]uint8{0, 11, 23, 26}
+var _TariffTypeIndex = [...]uint8{0, 11, 23, 36, 39}
 
-const _TariffTypeLowerName = "pricestaticpricedynamicco2"
+const _TariffTypeLowerName = "pricestaticpricedynamicpriceforecastco2"
 
 func (i TariffType) String() string {
 	i -= 1
@@ -27,24 +27,28 @@ func _TariffTypeNoOp() {
 	var x [1]struct{}
 	_ = x[TariffTypePriceStatic-(1)]
 	_ = x[TariffTypePriceDynamic-(2)]
-	_ = x[TariffTypeCo2-(3)]
+	_ = x[TariffTypePriceForecast-(3)]
+	_ = x[TariffTypeCo2-(4)]
 }
 
-var _TariffTypeValues = []TariffType{TariffTypePriceStatic, TariffTypePriceDynamic, TariffTypeCo2}
+var _TariffTypeValues = []TariffType{TariffTypePriceStatic, TariffTypePriceDynamic, TariffTypePriceForecast, TariffTypeCo2}
 
 var _TariffTypeNameToValueMap = map[string]TariffType{
 	_TariffTypeName[0:11]:       TariffTypePriceStatic,
 	_TariffTypeLowerName[0:11]:  TariffTypePriceStatic,
 	_TariffTypeName[11:23]:      TariffTypePriceDynamic,
 	_TariffTypeLowerName[11:23]: TariffTypePriceDynamic,
-	_TariffTypeName[23:26]:      TariffTypeCo2,
-	_TariffTypeLowerName[23:26]: TariffTypeCo2,
+	_TariffTypeName[23:36]:      TariffTypePriceForecast,
+	_TariffTypeLowerName[23:36]: TariffTypePriceForecast,
+	_TariffTypeName[36:39]:      TariffTypeCo2,
+	_TariffTypeLowerName[36:39]: TariffTypeCo2,
 }
 
 var _TariffTypeNames = []string{
 	_TariffTypeName[0:11],
 	_TariffTypeName[11:23],
-	_TariffTypeName[23:26],
+	_TariffTypeName[23:36],
+	_TariffTypeName[36:39],
 }
 
 // TariffTypeString retrieves an enum value from the enum constants string name.
diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue
index 30ac225c..b7b0ffdb 100644
--- a/assets/js/components/Energyflow/Energyflow.vue
+++ b/assets/js/components/Energyflow/Energyflow.vue
@@ -171,7 +171,7 @@ import GridSettingsModal from "../GridSettingsModal.vue";
 import formatter from "../../mixins/formatter";
 import AnimatedNumber from "../AnimatedNumber.vue";
 import settings from "../../settings";
-import { CO2_TYPE, PRICE_DYNAMIC_TYPE } from "../../units";
+import { CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE } from "../../units";
 import collector from "../../mixins/collector";
 import BatterySettingsModal from "../BatterySettingsModal.vue";
 
@@ -216,7 +216,7 @@ export default {
 	},
 	computed: {
 		smartCostAvailable: function () {
-			return [CO2_TYPE, PRICE_DYNAMIC_TYPE].includes(this.smartCostType);
+			return [CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE].includes(this.smartCostType);
 		},
 		gridImport: function () {
 			return Math.max(0, this.gridPower);
diff --git a/assets/js/components/GridSettingsModal.vue b/assets/js/components/GridSettingsModal.vue
index 0b6bb339..77e0f185 100644
--- a/assets/js/components/GridSettingsModal.vue
+++ b/assets/js/components/GridSettingsModal.vue
@@ -114,6 +114,7 @@ export default {
 	props: {
 		smartCostLimit: Number,
 		smartCostType: String,
+		tariffGrid: Number,
 		currency: String,
 	},
 	data: function () {
@@ -241,6 +242,11 @@ export default {
 				this.updateTariff();
 			}
 		},
+		tariffGrid() {
+			if (this.isModalVisible) {
+				this.updateTariff();
+			}
+		},
 		smartCostLimit(limit) {
 			this.selectedSmartCostLimit = limit;
 		},
diff --git a/assets/js/units.js b/assets/js/units.js
index 58ab3b8b..ca11d560 100644
--- a/assets/js/units.js
+++ b/assets/js/units.js
@@ -6,6 +6,7 @@ export const UNITS = [KM, MILES];
 
 export const CO2_TYPE = "co2";
 export const PRICE_DYNAMIC_TYPE = "pricedynamic";
+export const PRICE_FORECAST_TYPE = "priceforecast";
 
 const MILES_FACTOR = 0.6213711922;
 
diff --git a/charger/abb.go b/charger/abb.go
index b2ff703f..8fa80baa 100644
--- a/charger/abb.go
+++ b/charger/abb.go
@@ -218,12 +218,12 @@ func (wb *ABB) Currents() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var curr [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		curr[l] = float64(binary.BigEndian.Uint32(b[4*l:])) / 1e3
+		res[l] = float64(binary.BigEndian.Uint32(b[4*l:])) / 1e3
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.PhaseVoltages = (*ABB)(nil)
@@ -235,12 +235,12 @@ func (wb *ABB) Voltages() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var volt [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		volt[l] = float64(binary.BigEndian.Uint32(b[4*l:])) / 10
+		res[l] = float64(binary.BigEndian.Uint32(b[4*l:])) / 10
 	}
 
-	return volt[0], volt[1], volt[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 // var _ api.PhaseSwitcher = (*ABB)(nil)
diff --git a/charger/abl.go b/charger/abl.go
index 84012f2c..ea7b70ca 100644
--- a/charger/abl.go
+++ b/charger/abl.go
@@ -236,17 +236,17 @@ func (wb *ABLeMH) currents() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var currents []float64
-	for i := 2; i < 5; i++ {
-		u := binary.BigEndian.Uint16(b[2*i:])
+	var res [3]float64
+	for i := 0; i < 3; i++ {
+		u := binary.BigEndian.Uint16(b[2*(2+i):])
 		if u == ablAmpsDisabled || u == 1 {
 			u = 0
 		}
 
-		currents = append(currents, float64(u)/10)
+		res[i] = float64(u) / 10
 	}
 
-	return currents[2], currents[1], currents[0], nil
+	return res[2], res[1], res[0], nil
 }
 
 var _ api.Diagnosis = (*ABLeMH)(nil)
diff --git a/charger/bender.go b/charger/bender.go
index 45ea2d40..057ca532 100644
--- a/charger/bender.go
+++ b/charger/bender.go
@@ -287,12 +287,12 @@ func (wb *BenderCC) currents() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var curr [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		curr[l] = float64(binary.BigEndian.Uint32(b[4*l:4*(l+1)])) / 1e3
+		res[l] = float64(binary.BigEndian.Uint32(b[4*l:4*(l+1)])) / 1e3
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 // voltages implements the api.PhaseVoltages interface
@@ -302,12 +302,12 @@ func (wb *BenderCC) voltages() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var volt [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		volt[l] = float64(binary.BigEndian.Uint32(b[4*l : 4*(l+1)]))
+		res[l] = float64(binary.BigEndian.Uint32(b[4*l : 4*(l+1)]))
 	}
 
-	return volt[0], volt[1], volt[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 // identify implements the api.Identifier interface
diff --git a/charger/heidelberg-ec.go b/charger/heidelberg-ec.go
index ef6f8b05..8b007961 100644
--- a/charger/heidelberg-ec.go
+++ b/charger/heidelberg-ec.go
@@ -271,12 +271,12 @@ func (wb *HeidelbergEC) Currents() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var curr [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		curr[l] = float64(binary.BigEndian.Uint16(b[2*l:])) / 10
+		res[l] = float64(binary.BigEndian.Uint16(b[2*l:])) / 10
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.PhaseVoltages = (*HeidelbergEC)(nil)
@@ -288,12 +288,12 @@ func (wb *HeidelbergEC) Voltages() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var volt [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		volt[l] = float64(binary.BigEndian.Uint16(b[2*l:]))
+		res[l] = float64(binary.BigEndian.Uint16(b[2*l:]))
 	}
 
-	return volt[0], volt[1], volt[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Diagnosis = (*HeidelbergEC)(nil)
diff --git a/charger/openwb.go b/charger/openwb.go
index 8f67e0ca..ff257ca3 100644
--- a/charger/openwb.go
+++ b/charger/openwb.go
@@ -214,17 +214,17 @@ var _ api.PhaseCurrents = (*OpenWB)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (m *OpenWB) Currents() (float64, float64, float64, error) {
-	var currents []float64
+	var res []float64
 	for _, currentG := range m.currentsG {
 		c, err := currentG()
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		currents = append(currents, c)
+		res = append(res, c)
 	}
 
-	return currents[0], currents[1], currents[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Authorizer = (*OpenWB)(nil)
diff --git a/charger/smaevcharger.go b/charger/smaevcharger.go
index 37502204..f557b81c 100644
--- a/charger/smaevcharger.go
+++ b/charger/smaevcharger.go
@@ -252,18 +252,18 @@ var _ api.PhaseCurrents = (*Smaevcharger)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (wb *Smaevcharger) Currents() (float64, float64, float64, error) {
-	var curr []float64
+	var res [3]float64
 
-	for _, phase := range []string{"A", "B", "C"} {
+	for i, phase := range []string{"A", "B", "C"} {
 		val, err := wb.getMeasurement("Measurement.GridMs.A.phs" + phase)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		curr = append(curr, -val)
+		res[i] = -val
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 // reset cache
diff --git a/charger/vestel.go b/charger/vestel.go
index b4e1a223..38bb75d2 100644
--- a/charger/vestel.go
+++ b/charger/vestel.go
@@ -42,11 +42,13 @@ const (
 	vestelRegSessionEnergy   = 1502
 	vestelRegFailsafeTimeout = 2002
 	vestelRegAlive           = 6000
-	//vestelRegChargepointState = 1000
+	// vestelRegChargepointState = 1000
 )
 
-var vestelRegCurrents = []uint16{1008, 1010, 1012} // non-continuous uint16 registers!
-var vestelRegVoltages = []uint16{1014, 1016, 1018} // non-continuous uint16 registers!
+var (
+	vestelRegCurrents = []uint16{1008, 1010, 1012} // non-continuous uint16 registers!
+	vestelRegVoltages = []uint16{1014, 1016, 1018} // non-continuous uint16 registers!
+)
 
 // Vestel is an api.Charger implementation for Vestel/Hymes wallboxes with Ethernet (SW modells).
 // It uses Modbus TCP to communicate with the wallbox at modbus client id 255.
@@ -212,34 +214,34 @@ var _ api.PhaseCurrents = (*Vestel)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (wb *Vestel) Currents() (float64, float64, float64, error) {
-	var currents []float64
-	for _, regCurrent := range vestelRegCurrents {
+	var res [3]float64
+	for i, regCurrent := range vestelRegCurrents {
 		b, err := wb.conn.ReadInputRegisters(regCurrent, 1)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		currents = append(currents, float64(binary.BigEndian.Uint16(b))/1e3)
+		res[i] = float64(binary.BigEndian.Uint16(b)) / 1e3
 	}
 
-	return currents[0], currents[1], currents[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.PhaseVoltages = (*Vestel)(nil)
 
 // Voltages implements the api.PhaseVoltages interface
 func (wb *Vestel) Voltages() (float64, float64, float64, error) {
-	var voltages []float64
-	for _, regVoltage := range vestelRegVoltages {
+	var res [3]float64
+	for i, regVoltage := range vestelRegVoltages {
 		b, err := wb.conn.ReadInputRegisters(regVoltage, 1)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		voltages = append(voltages, float64(binary.BigEndian.Uint16(b)))
+		res[i] = float64(binary.BigEndian.Uint16(b))
 	}
 
-	return voltages[0], voltages[1], voltages[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Diagnosis = (*Vestel)(nil)
diff --git a/charger/wallbe.go b/charger/wallbe.go
index 8b5a1f4d..9f43c159 100644
--- a/charger/wallbe.go
+++ b/charger/wallbe.go
@@ -195,17 +195,17 @@ func (wb *Wallbe) totalEnergy() (float64, error) {
 
 // currents implements the api.PhaseCurrents interface
 func (wb *Wallbe) currents() (float64, float64, float64, error) {
-	var currents []float64
-	for _, regCurrent := range wbRegCurrents {
-		b, err := wb.conn.ReadInputRegisters(regCurrent, 2)
+	var res [3]float64
+	for i, reg := range wbRegCurrents {
+		b, err := wb.conn.ReadInputRegisters(reg, 2)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		currents = append(currents, rs485.RTUInt32ToFloat64Swapped(b))
+		res[i] = rs485.RTUInt32ToFloat64Swapped(b)
 	}
 
-	return currents[0], currents[1], currents[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Diagnosis = (*Wallbe)(nil)
diff --git a/charger/webasto-next.go b/charger/webasto-next.go
index 6b2c7030..57871405 100644
--- a/charger/webasto-next.go
+++ b/charger/webasto-next.go
@@ -217,17 +217,17 @@ var _ api.PhaseCurrents = (*WebastoNext)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (wb *WebastoNext) Currents() (float64, float64, float64, error) {
-	var curr [3]float64
+	var res [3]float64
 	for l := uint16(0); l < 3; l++ {
 		b, err := wb.conn.ReadInputRegisters(tqRegCurrents+2*l, 1)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		curr[l] = float64(binary.BigEndian.Uint16(b)) / 1e3
+		res[l] = float64(binary.BigEndian.Uint16(b)) / 1e3
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Identifier = (*WebastoNext)(nil)
diff --git a/core/loadpoint.go b/core/loadpoint.go
index bd4ad667..4e6b58e7 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -174,7 +174,7 @@ type Loadpoint struct {
 	progress                *Progress      // Step-wise progress indicator
 
 	// session log
-	db      session.Database
+	db      *session.DB
 	session *session.Session
 
 	tasks *util.Queue[Task] // tasks to be executed
diff --git a/core/session/db.go b/core/session/db.go
index 9c3e70a8..6c910471 100644
--- a/core/session/db.go
+++ b/core/session/db.go
@@ -12,11 +12,6 @@ type DB struct {
 	name string
 }
 
-type Database interface {
-	New(startEnergy float64) *Session
-	Persist(session interface{})
-}
-
 // NewStore creates a session store
 func NewStore(name string, db *gorm.DB) (*DB, error) {
 	err := db.AutoMigrate(new(Session))
@@ -57,3 +52,31 @@ func (s *DB) Sessions() (Sessions, error) {
 	tx := s.db.Find(&res)
 	return res, tx.Error
 }
+
+func (s *DB) ClosePendingSessionsInHistory(chargeMeterTotal float64) error {
+	var res Sessions
+	if tx := s.db.Find(&res, map[string]interface{}{"finished": "0001-01-01 00:00:00+00:00", "Loadpoint": s.name}); tx.Error != nil {
+		return tx.Error
+	}
+
+	for _, session := range res {
+		var nextSession Session
+
+		var tx *gorm.DB
+		if tx = s.db.Limit(1).Order("ID").Find(&nextSession, "ID > ? AND Loadpoint = ?", session.ID, s.name); tx.Error != nil {
+			return tx.Error
+		}
+
+		if tx.RowsAffected == 0 {
+			// no successor, this is the most recent session and it is open
+			session.MeterStop = &chargeMeterTotal
+		} else {
+			session.MeterStop = nextSession.MeterStart
+		}
+
+		session.ChargedEnergy = *session.MeterStop - *session.MeterStart
+		s.Persist(session)
+	}
+
+	return nil
+}
diff --git a/core/site.go b/core/site.go
index f59f7349..125748f8 100644
--- a/core/site.go
+++ b/core/site.go
@@ -142,6 +142,10 @@ func NewSiteFromConfig(
 			if lp.db, err = session.NewStore(lp.Title(), db.Instance); err != nil {
 				return nil, err
 			}
+			// Fix any dangling history
+			if err := lp.db.ClosePendingSessionsInHistory(lp.chargeMeterTotal()); err != nil {
+				return nil, err
+			}
 
 			// NOTE: this requires stopSession to respect async access
 			shutdown.Register(lp.stopSession)
diff --git a/core/site_api.go b/core/site_api.go
index f7d4ba7e..6be6cdeb 100644
--- a/core/site_api.go
+++ b/core/site_api.go
@@ -147,7 +147,7 @@ func (site *Site) GetTariff(tariff string) api.Tariff {
 			// prio 0: manually set planner tariff
 			return site.tariffs.Planner
 
-		case site.tariffs.Grid != nil && site.tariffs.Grid.Type() == api.TariffTypePriceDynamic:
+		case site.tariffs.Grid != nil && site.tariffs.Grid.Type() == api.TariffTypePriceForecast:
 			// prio 1: dynamic grid tariff
 			return site.tariffs.Grid
 
diff --git a/evcc.dist.yaml b/evcc.dist.yaml
index 990598d2..79b79752 100644
--- a/evcc.dist.yaml
+++ b/evcc.dist.yaml
@@ -194,6 +194,13 @@ tariffs:
     # securitytoken: # api token
     # charges: # optional, additional charges per kWh
     # tax: # optional, additional tax (0.1 for 10%)
+
+    # type: custom # price from a plugin source; see https://docs.evcc.io/docs/reference/plugins
+    # price:
+    #   source: http
+    #   uri: https://example.org/price.json
+    #   jq: .price.current
+
   feedin:
     # rate for feeding excess (pv) energy to the grid
     type: fixed
diff --git a/meter/sma.go b/meter/sma.go
index dd9e806b..3a8e35ad 100644
--- a/meter/sma.go
+++ b/meter/sma.go
@@ -125,12 +125,12 @@ func (sm *SMA) Currents() (float64, float64, float64, error) {
 		}
 	}
 
-	var currents [3]float64
+	var res [3]float64
 	for i, id := range []sunny.ValueID{sunny.CurrentL1, sunny.CurrentL2, sunny.CurrentL3} {
-		currents[i] = util.SignFromPower(sma.AsFloat(values[id]), powers[i])
+		res[i] = util.SignFromPower(sma.AsFloat(values[id]), powers[i])
 	}
 
-	return currents[0], currents[1], currents[2], err
+	return res[0], res[1], res[2], err
 }
 
 var _ api.PhaseVoltages = (*SMA)(nil)
@@ -139,12 +139,12 @@ var _ api.PhaseVoltages = (*SMA)(nil)
 func (sm *SMA) Voltages() (float64, float64, float64, error) {
 	values, err := sm.device.Values()
 
-	var voltages [3]float64
+	var res [3]float64
 	for i, id := range []sunny.ValueID{sunny.VoltageL1, sunny.VoltageL2, sunny.VoltageL3} {
-		voltages[i] = sma.AsFloat(values[id])
+		res[i] = sma.AsFloat(values[id])
 	}
 
-	return voltages[0], voltages[1], voltages[2], err
+	return res[0], res[1], res[2], err
 }
 
 var _ api.PhasePowers = (*SMA)(nil)
@@ -153,15 +153,15 @@ var _ api.PhasePowers = (*SMA)(nil)
 func (sm *SMA) Powers() (float64, float64, float64, error) {
 	values, err := sm.device.Values()
 
-	var powers [3]float64
+	var res [3]float64
 	for i, id := range []sunny.ValueID{sunny.ActivePowerPlusL1, sunny.ActivePowerPlusL2, sunny.ActivePowerPlusL3} {
-		powers[i] = sma.AsFloat(values[id])
+		res[i] = sma.AsFloat(values[id])
 	}
 	for i, id := range []sunny.ValueID{sunny.ActivePowerMinusL1, sunny.ActivePowerMinusL2, sunny.ActivePowerMinusL3} {
-		powers[i] -= sma.AsFloat(values[id])
+		res[i] -= sma.AsFloat(values[id])
 	}
 
-	return powers[0], powers[1], powers[2], err
+	return res[0], res[1], res[2], err
 }
 
 // soc implements the api.Battery interface
diff --git a/tariff/awattar.go b/tariff/awattar.go
index 3949575e..5b94f43c 100644
--- a/tariff/awattar.go
+++ b/tariff/awattar.go
@@ -99,5 +99,5 @@ func (t *Awattar) Rates() (api.Rates, error) {
 
 // Type implements the api.Tariff interface
 func (t *Awattar) Type() api.TariffType {
-	return api.TariffTypePriceDynamic
+	return api.TariffTypePriceForecast
 }
diff --git a/tariff/elering.go b/tariff/elering.go
index 99105168..3dd1a766 100644
--- a/tariff/elering.go
+++ b/tariff/elering.go
@@ -112,5 +112,5 @@ func (t *Elering) Rates() (api.Rates, error) {
 
 // Type implements the api.Tariff interface
 func (t *Elering) Type() api.TariffType {
-	return api.TariffTypePriceDynamic
+	return api.TariffTypePriceForecast
 }
diff --git a/tariff/energinet.go b/tariff/energinet.go
index d3ddaaf3..c5ec71c1 100644
--- a/tariff/energinet.go
+++ b/tariff/energinet.go
@@ -109,5 +109,5 @@ func (t *Energinet) Rates() (api.Rates, error) {
 
 // Type implements the api.Tariff interface
 func (t *Energinet) Type() api.TariffType {
-	return api.TariffTypePriceDynamic
+	return api.TariffTypePriceForecast
 }
diff --git a/tariff/fixed.go b/tariff/fixed.go
index 11f6a36a..0c1a41b4 100644
--- a/tariff/fixed.go
+++ b/tariff/fixed.go
@@ -133,7 +133,7 @@ func (t *Fixed) Rates() (api.Rates, error) {
 // Type implements the api.Tariff interface
 func (t *Fixed) Type() api.TariffType {
 	if t.dynamic {
-		return api.TariffTypePriceDynamic
+		return api.TariffTypePriceForecast
 	}
 	return api.TariffTypePriceStatic
 }
diff --git a/tariff/octopus.go b/tariff/octopus.go
index 14867ef1..33ff2278 100644
--- a/tariff/octopus.go
+++ b/tariff/octopus.go
@@ -110,5 +110,5 @@ func (t *Octopus) Rates() (api.Rates, error) {
 
 // Type implements the api.Tariff interface
 func (t *Octopus) Type() api.TariffType {
-	return api.TariffTypePriceDynamic
+	return api.TariffTypePriceForecast
 }
diff --git a/tariff/tariff.go b/tariff/tariff.go
new file mode 100644
index 00000000..ba759c4d
--- /dev/null
+++ b/tariff/tariff.go
@@ -0,0 +1,68 @@
+package tariff
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/provider"
+	"github.com/evcc-io/evcc/util"
+	"github.com/jinzhu/now"
+)
+
+type Tariff struct {
+	*embed
+	priceG func() (float64, error)
+}
+
+var _ api.Tariff = (*Tariff)(nil)
+
+func init() {
+	registry.Add(api.Custom, NewConfigurableFromConfig)
+}
+
+func NewConfigurableFromConfig(other map[string]interface{}) (api.Tariff, error) {
+	var cc struct {
+		embed `mapstructure:",squash"`
+		Price provider.Config
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	priceG, err := provider.NewFloatGetterFromConfig(cc.Price)
+	if err != nil {
+		return nil, fmt.Errorf("price: %w", err)
+	}
+
+	t := &Tariff{
+		embed:  &cc.embed,
+		priceG: priceG,
+	}
+
+	return t, nil
+}
+
+// Rates implements the api.Tariff interface
+func (t *Tariff) Rates() (api.Rates, error) {
+	price, err := t.priceG()
+	if err != nil {
+		return nil, err
+	}
+
+	res := api.Rates{
+		{
+			Start: now.BeginningOfHour(),
+			End:   now.BeginningOfHour().Add(48 * time.Hour),
+			Price: t.totalPrice(price),
+		},
+	}
+
+	return res, nil
+}
+
+// Type implements the api.Tariff interface
+func (t *Tariff) Type() api.TariffType {
+	return api.TariffTypePriceDynamic
+}
diff --git a/tariff/tibber.go b/tariff/tibber.go
index e54c4f3a..b1978b0e 100644
--- a/tariff/tibber.go
+++ b/tariff/tibber.go
@@ -140,5 +140,5 @@ func (t *Tibber) Rates() (api.Rates, error) {
 
 // Type implements the api.Tariff interface
 func (t *Tibber) Type() api.TariffType {
-	return api.TariffTypePriceDynamic
+	return api.TariffTypePriceForecast
 }


Test patch:
diff --git a/core/loadpoint_session_test.go b/core/loadpoint_session_test.go
index aeb63749..fdc9a1e7 100644
--- a/core/loadpoint_session_test.go
+++ b/core/loadpoint_session_test.go
@@ -87,3 +87,99 @@ func TestSession(t *testing.T) {
 	assert.Len(t, s, 1)
 	t.Logf("session: %+v", s)
 }
+
+func TestCloseSessionsOnStartup_emptyDb(t *testing.T) {
+	var err error
+	serverdb.Instance, err = serverdb.New("sqlite", ":memory:")
+	assert.NoError(t, err)
+
+	db, err := session.NewStore("foo", serverdb.Instance)
+	assert.NoError(t, err)
+
+	// assert empty DB is no problem
+	err = db.ClosePendingSessionsInHistory(1000)
+	assert.NoError(t, err)
+}
+
+func TestCloseSessionsOnStartup(t *testing.T) {
+	var err error
+	serverdb.Instance, err = serverdb.New("sqlite", ":memory:")
+	assert.NoError(t, err)
+
+	db1, err := session.NewStore("foo", serverdb.Instance)
+	assert.NoError(t, err)
+
+	db2, err := session.NewStore("bar", serverdb.Instance)
+	assert.NoError(t, err)
+
+	clock := clock.NewMock()
+
+	//test data, creates 6 sessions for each loadpoint, 3rd and 6th are "unfinished"
+	var sessions1 []*session.Session = createMockSessions(db1, clock)
+	var sessions2 []*session.Session = createMockSessions(db2, clock)
+
+	//write interleaved for two loadpoints
+	for index, session := range sessions1 {
+		db1.Persist(session)
+		db2.Persist(sessions2[index])
+	}
+
+	err = db1.ClosePendingSessionsInHistory(1000)
+	assert.NoError(t, err)
+
+	//check fixed sessions for db1
+	var db1Sessions session.Sessions
+	err = serverdb.Instance.Where("Loadpoint = ?", "foo").Order("ID").Find(&db1Sessions).Error
+	assert.NoError(t, err)
+	assert.Len(t, db1Sessions, 6)
+
+	//check fixed history
+	for _, s := range db1Sessions[:5] {
+		assert.NotEmpty(t, s.MeterStop)
+		assert.Equal(t, float64(10), s.ChargedEnergy)
+		t.Logf("session: %+v", s)
+	}
+
+	//check fixed most recent record
+	assert.NotEmpty(t, db1Sessions[5].MeterStop)
+	assert.Equal(t, float64(940), db1Sessions[5].ChargedEnergy)
+
+	//ensure no side effects on loadpoint 2 data, i.e. data left unfixed
+	var db2Sessions session.Sessions
+	err = serverdb.Instance.Where("Loadpoint = ?", "bar").Order("ID").Find(&db2Sessions).Error
+	assert.NoError(t, err)
+	assert.Len(t, db2Sessions, 6)
+
+	for i, s := range db2Sessions {
+		if (i+1)%3 == 0 {
+			assert.Empty(t, s.MeterStop)
+			assert.Empty(t, s.ChargedEnergy)
+			continue
+		}
+		assert.NotEmpty(t, s.MeterStop)
+		assert.Equal(t, float64(10), s.ChargedEnergy)
+	}
+}
+
+func createMockSessions(db *session.DB, clock *clock.Mock) []*session.Session {
+	var sessions []*session.Session
+	for i := 1; i <= 6; i++ {
+
+		var meter1Start float64 = float64(i * 10)
+		session := db.New(meter1Start)
+		session.Created = clock.Now().Add(1 * time.Minute)
+
+		//create every third session as incomplete
+		if i%3 == 0 {
+			sessions = append(sessions, session)
+			continue
+		}
+
+		session.Finished = clock.Now().Add(2 * time.Minute)
+		meterStop := float64(meter1Start + 10)
+		session.MeterStop = &meterStop
+		session.ChargedEnergy = 10
+		sessions = append(sessions, session)
+	}
+	return sessions
+}