590 lines
14 KiB
Go
590 lines
14 KiB
Go
package core
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/core/keys"
|
|
"github.com/evcc-io/evcc/core/loadpoint"
|
|
"github.com/evcc-io/evcc/core/site"
|
|
"github.com/evcc-io/evcc/server/db/settings"
|
|
"github.com/evcc-io/evcc/util/config"
|
|
"github.com/evcc-io/evcc/util/sponsor"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
var _ site.API = (*Site)(nil)
|
|
|
|
var (
|
|
ErrBatteryNotConfigured = errors.New("battery not configured")
|
|
ErrBatteryControlNotAvailable = errors.New("battery control not available")
|
|
)
|
|
|
|
// isConfigurable checks if the meter is configurable
|
|
func isConfigurable(ref string) bool {
|
|
dev, _ := config.Meters().ByName(ref)
|
|
_, ok := dev.(config.ConfigurableDevice[api.Meter])
|
|
return ok
|
|
}
|
|
|
|
// filterConfigurable filters configurable meters
|
|
func filterConfigurable(ref []string) []string {
|
|
return lo.Filter(ref, func(ref string, _ int) bool {
|
|
return isConfigurable(ref)
|
|
})
|
|
}
|
|
|
|
// Optimize updates the optimizer
|
|
func (site *Site) Optimize() error {
|
|
if !sponsor.IsAuthorized() || !optimizerEnabled() {
|
|
return api.ErrNotAvailable
|
|
}
|
|
|
|
go site.optimizerUpdateAsync()
|
|
return nil
|
|
}
|
|
|
|
// GetTitle returns the title
|
|
func (site *Site) GetTitle() string {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.Title
|
|
}
|
|
|
|
// SetTitle sets the title
|
|
func (site *Site) SetTitle(title string) {
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
site.Title = title
|
|
site.publish(keys.SiteTitle, title)
|
|
settings.SetString(keys.Title, title)
|
|
}
|
|
|
|
// GetGridMeterRef returns the GridMeterRef
|
|
func (site *Site) GetGridMeterRef() string {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.Meters.GridMeterRef
|
|
}
|
|
|
|
// SetGridMeterRef sets the GridMeterRef
|
|
func (site *Site) SetGridMeterRef(ref string) {
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
site.Meters.GridMeterRef = ref
|
|
settings.SetString(keys.GridMeter, ref)
|
|
}
|
|
|
|
// GetPVMeterRefs returns the PvMeterRef
|
|
func (site *Site) GetPVMeterRefs() []string {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.Meters.PVMetersRef
|
|
}
|
|
|
|
// SetPVMeterRefs sets the PvMeterRef
|
|
func (site *Site) SetPVMeterRefs(ref []string) {
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
site.Meters.PVMetersRef = ref
|
|
settings.SetString(keys.PvMeters, strings.Join(filterConfigurable(ref), ","))
|
|
}
|
|
|
|
// GetBatteryMeterRefs returns the BatteryMeterRef
|
|
func (site *Site) GetBatteryMeterRefs() []string {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.Meters.BatteryMetersRef
|
|
}
|
|
|
|
// SetBatteryMeterRefs sets the BatteryMeterRef
|
|
func (site *Site) SetBatteryMeterRefs(ref []string) {
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
site.Meters.BatteryMetersRef = ref
|
|
settings.SetString(keys.BatteryMeters, strings.Join(filterConfigurable(ref), ","))
|
|
}
|
|
|
|
// GetAuxMeterRefs returns the AuxMeterRef
|
|
func (site *Site) GetAuxMeterRefs() []string {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.Meters.AuxMetersRef
|
|
}
|
|
|
|
// SetAuxMeterRefs sets the AuxMeterRef
|
|
func (site *Site) SetAuxMeterRefs(ref []string) {
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
site.Meters.AuxMetersRef = ref
|
|
settings.SetString(keys.AuxMeters, strings.Join(filterConfigurable(ref), ","))
|
|
}
|
|
|
|
// GetConsumerMeterRefs returns the ConsumerMeterRef
|
|
func (site *Site) GetConsumerMeterRefs() []string {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.Meters.ConsumerMetersRef
|
|
}
|
|
|
|
// SetConsumerMeterRefs sets the ConsumerMeterRef
|
|
func (site *Site) SetConsumerMeterRefs(ref []string) {
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
site.Meters.ConsumerMetersRef = ref
|
|
settings.SetString(keys.ConsumerMeters, strings.Join(filterConfigurable(ref), ","))
|
|
}
|
|
|
|
// GetExtMeterRefs returns the ExtMeterRef
|
|
func (site *Site) GetExtMeterRefs() []string {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.Meters.ExtMetersRef
|
|
}
|
|
|
|
// SetExtMeterRefs sets the ExtMeterRef
|
|
func (site *Site) SetExtMeterRefs(ref []string) {
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
site.Meters.ExtMetersRef = ref
|
|
settings.SetString(keys.ExtMeters, strings.Join(filterConfigurable(ref), ","))
|
|
}
|
|
|
|
// GetBatterySoc returns the current battery soc
|
|
func (site *Site) GetBatterySoc() float64 {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.battery.Soc
|
|
}
|
|
|
|
// GetBatteryMaxDischargePower returns the current battery max discharge power
|
|
func (site *Site) GetBatteryMaxDischargePower() float64 {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.batteryMaxDischargePower
|
|
}
|
|
|
|
// Loadpoints returns the loadpoints as api interfaces
|
|
func (site *Site) Loadpoints() []loadpoint.API {
|
|
return lo.Map(site.loadpoints, func(lp *Loadpoint, _ int) loadpoint.API { return lp })
|
|
}
|
|
|
|
func (site *Site) hasMeters() bool {
|
|
return site.gridMeter != nil || len(site.pvMeters) > 0 || len(site.batteryMeters) > 0 || len(site.auxMeters) > 0 || len(site.extMeters) > 0
|
|
}
|
|
|
|
func (site *Site) IsConfigured() bool {
|
|
return len(site.loadpoints) > 0 || site.hasMeters()
|
|
}
|
|
|
|
// loadpointsAsCircuitDevices returns the loadpoints as circuit devices
|
|
func (site *Site) loadpointsAsCircuitDevices() []api.CircuitLoad {
|
|
return lo.Map(site.loadpoints, func(lp *Loadpoint, _ int) api.CircuitLoad { return lp })
|
|
}
|
|
|
|
// Vehicles returns the site vehicles
|
|
func (site *Site) Vehicles() site.Vehicles {
|
|
return &vehicles{log: site.log}
|
|
}
|
|
|
|
// GetCircuit returns the root circuit
|
|
func (site *Site) GetCircuit() api.Circuit {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.circuit
|
|
}
|
|
|
|
// SetHEMS attaches the configured HEMS to the site and the root circuit
|
|
func (site *Site) SetHEMS(hems api.HEMS) {
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
site.hems = hems
|
|
|
|
if site.circuit != nil {
|
|
site.circuit.SetHEMS(hems)
|
|
}
|
|
}
|
|
|
|
// GetPrioritySoc returns the PrioritySoc
|
|
func (site *Site) GetPrioritySoc() float64 {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.prioritySoc
|
|
}
|
|
|
|
// SetPrioritySoc sets the PrioritySoc
|
|
func (site *Site) SetPrioritySoc(soc float64) error {
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
if len(site.batteryMeters) == 0 {
|
|
return ErrBatteryNotConfigured
|
|
}
|
|
|
|
if site.bufferSoc != 0 && soc > site.bufferSoc {
|
|
return errors.New("priority soc must be smaller or equal than buffer soc")
|
|
}
|
|
|
|
site.log.DEBUG.Println("set priority soc:", soc)
|
|
|
|
if site.prioritySoc != soc {
|
|
site.prioritySoc = soc
|
|
settings.SetFloat(keys.PrioritySoc, site.prioritySoc)
|
|
site.publish(keys.PrioritySoc, site.prioritySoc)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetBufferSoc returns the BufferSoc
|
|
func (site *Site) GetBufferSoc() float64 {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.bufferSoc
|
|
}
|
|
|
|
// SetBufferSoc sets the BufferSoc
|
|
func (site *Site) SetBufferSoc(soc float64) error {
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
if len(site.batteryMeters) == 0 {
|
|
return ErrBatteryNotConfigured
|
|
}
|
|
|
|
if soc != 0 && soc < site.prioritySoc {
|
|
return errors.New("buffer soc must not be smaller than priority soc")
|
|
}
|
|
|
|
if site.bufferStartSoc != 0 && soc > site.bufferStartSoc {
|
|
return errors.New("buffer soc must be smaller or equal than buffer start soc")
|
|
}
|
|
|
|
site.log.DEBUG.Println("set buffer soc:", soc)
|
|
|
|
if site.bufferSoc != soc {
|
|
site.bufferSoc = soc
|
|
settings.SetFloat(keys.BufferSoc, site.bufferSoc)
|
|
site.publish(keys.BufferSoc, site.bufferSoc)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetBufferStartSoc returns the BufferStartSoc
|
|
func (site *Site) GetBufferStartSoc() float64 {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.bufferStartSoc
|
|
}
|
|
|
|
// SetBufferStartSoc sets the BufferStartSoc
|
|
func (site *Site) SetBufferStartSoc(soc float64) error {
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
if len(site.batteryMeters) == 0 {
|
|
return ErrBatteryNotConfigured
|
|
}
|
|
|
|
if soc != 0 && soc < site.bufferSoc {
|
|
return errors.New("buffer start soc must be larger than buffer soc")
|
|
}
|
|
|
|
site.log.DEBUG.Println("set buffer start soc:", soc)
|
|
|
|
if site.bufferStartSoc != soc {
|
|
site.bufferStartSoc = soc
|
|
settings.SetFloat(keys.BufferStartSoc, site.bufferStartSoc)
|
|
site.publish(keys.BufferStartSoc, site.bufferStartSoc)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetGridPower returns the most recent grid power reading in W (positive = import)
|
|
func (site *Site) GetGridPower() float64 {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.gridPower
|
|
}
|
|
|
|
// GetResidualPower returns the ResidualPower
|
|
func (site *Site) GetResidualPower() float64 {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.ResidualPower
|
|
}
|
|
|
|
// SetResidualPower sets the ResidualPower
|
|
func (site *Site) SetResidualPower(power float64) error {
|
|
site.log.DEBUG.Println("set residual power:", power)
|
|
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
if site.ResidualPower != power {
|
|
site.ResidualPower = power
|
|
settings.SetFloat(keys.ResidualPower, site.ResidualPower)
|
|
site.publish(keys.ResidualPower, site.ResidualPower)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetGridExportLimit returns the static grid export power limit in W (0 = disabled)
|
|
func (site *Site) GetGridExportLimit() float64 {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.gridExportLimit
|
|
}
|
|
|
|
// SetGridExportLimit sets the static grid export power limit in W (0 = disabled)
|
|
func (site *Site) SetGridExportLimit(power float64) error {
|
|
if power < 0 {
|
|
return fmt.Errorf("invalid grid export limit: %g", power)
|
|
}
|
|
|
|
site.Lock()
|
|
changed := site.gridExportLimit != power
|
|
if changed {
|
|
site.gridExportLimit = power
|
|
}
|
|
site.Unlock()
|
|
|
|
if changed {
|
|
site.log.DEBUG.Println("set grid export limit:", power)
|
|
settings.SetFloat(keys.GridExportLimit, power)
|
|
site.publish(keys.GridExportLimit, power)
|
|
|
|
// re-run the optimizer so the new limit takes effect immediately
|
|
site.triggerOptimizer()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetTariff returns the respective tariff if configured or nil
|
|
func (site *Site) GetTariff(tariff api.TariffUsage) api.Tariff {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.tariffs.Get(tariff)
|
|
}
|
|
|
|
// GetBatteryDischargeControl returns the battery control mode (no discharge only)
|
|
func (site *Site) GetBatteryDischargeControl() bool {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.batteryDischargeControl
|
|
}
|
|
|
|
// SetBatteryDischargeControl sets the battery control mode (no discharge only)
|
|
func (site *Site) SetBatteryDischargeControl(val bool) error {
|
|
site.log.DEBUG.Println("set battery discharge control:", val)
|
|
|
|
if !site.hasBatteryControl() {
|
|
return ErrBatteryControlNotAvailable
|
|
}
|
|
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
if site.batteryDischargeControl != val {
|
|
site.batteryDischargeControl = val
|
|
settings.SetBool(keys.BatteryDischargeControl, val)
|
|
site.publish(keys.BatteryDischargeControl, val)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetBatteryGridDischarge returns whether the battery may discharge to grid (experimental)
|
|
func (site *Site) GetBatteryGridDischarge() bool {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.batteryGridDischarge
|
|
}
|
|
|
|
// SetBatteryGridDischarge sets whether the battery may discharge to grid (experimental)
|
|
func (site *Site) SetBatteryGridDischarge(val bool) error {
|
|
site.log.DEBUG.Println("set battery grid discharge:", val)
|
|
|
|
if !site.hasBatteryControl() {
|
|
return ErrBatteryControlNotAvailable
|
|
}
|
|
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
if site.batteryGridDischarge != val {
|
|
site.batteryGridDischarge = val
|
|
settings.SetBool(keys.BatteryGridDischarge, val)
|
|
site.publish(keys.BatteryGridDischarge, val)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetSolarAdjusted returns if the solar forecast is adjusted to real production data
|
|
func (site *Site) GetSolarAdjusted() bool {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.solarAdjusted
|
|
}
|
|
|
|
// SetSolarAdjusted sets if the solar forecast is adjusted to real production data
|
|
func (site *Site) SetSolarAdjusted(val bool) {
|
|
site.log.DEBUG.Println("set solar adjusted:", val)
|
|
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
if site.solarAdjusted != val {
|
|
site.solarAdjusted = val
|
|
settings.SetBool(keys.SolarAdjusted, val)
|
|
site.publish(keys.SolarAdjusted, val)
|
|
}
|
|
}
|
|
|
|
func (site *Site) GetBatteryGridChargeLimit() *float64 {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.batteryGridChargeLimit
|
|
}
|
|
|
|
func (site *Site) SetBatteryGridChargeLimit(val *float64) error {
|
|
site.log.DEBUG.Println("set grid charge limit:", printPtr("%.1f", val))
|
|
|
|
if !site.hasBatteryControl() {
|
|
return ErrBatteryControlNotAvailable
|
|
}
|
|
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
if !ptrValueEqual(site.batteryGridChargeLimit, val) {
|
|
site.batteryGridChargeLimit = val
|
|
|
|
if val == nil {
|
|
settings.SetString(keys.BatteryGridChargeLimit, "")
|
|
site.publish(keys.BatteryGridChargeLimit, nil)
|
|
} else {
|
|
settings.SetFloat(keys.BatteryGridChargeLimit, *val)
|
|
site.publish(keys.BatteryGridChargeLimit, *val)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetOptimizerChargingStrategy returns the optimizer grid charging strategy,
|
|
// falling back to the default when unset.
|
|
func (site *Site) GetOptimizerChargingStrategy() string {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
if site.optimizerChargingStrategy == "" {
|
|
return defaultOptimizerChargingStrategy
|
|
}
|
|
return site.optimizerChargingStrategy
|
|
}
|
|
|
|
// SetOptimizerChargingStrategy validates and persists the optimizer grid
|
|
// charging strategy and re-runs the optimizer when it changes.
|
|
func (site *Site) SetOptimizerChargingStrategy(strategy string) error {
|
|
if !slices.Contains(optimizerChargingStrategies, strategy) {
|
|
return fmt.Errorf("invalid optimizer charging strategy: %s", strategy)
|
|
}
|
|
|
|
site.Lock()
|
|
changed := site.optimizerChargingStrategy != strategy
|
|
if changed {
|
|
site.optimizerChargingStrategy = strategy
|
|
}
|
|
site.Unlock()
|
|
|
|
if changed {
|
|
site.log.DEBUG.Println("set optimizer charging strategy:", strategy)
|
|
settings.SetString(keys.OptimizerChargingStrategy, strategy)
|
|
site.publish(keys.OptimizerChargingStrategy, strategy)
|
|
|
|
// re-run the optimizer so the new strategy takes effect immediately
|
|
site.triggerOptimizer()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetBatteryMode returns the battery mode
|
|
func (site *Site) GetBatteryMode() api.BatteryMode {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.batteryMode
|
|
}
|
|
|
|
// GetBatteryModeExternal returns the external battery mode
|
|
func (site *Site) GetBatteryModeExternal() api.BatteryMode {
|
|
site.RLock()
|
|
defer site.RUnlock()
|
|
return site.batteryModeExternal
|
|
}
|
|
|
|
// SetBatteryModeExternal sets the external battery mode
|
|
func (site *Site) SetBatteryModeExternal(mode api.BatteryMode) error {
|
|
site.log.DEBUG.Printf("set external battery mode: %s", mode.String())
|
|
|
|
if !site.hasBatteryControl() {
|
|
return ErrBatteryControlNotAvailable
|
|
}
|
|
|
|
site.Lock()
|
|
defer site.Unlock()
|
|
|
|
disable := mode == api.BatteryUnknown
|
|
|
|
if mode != site.batteryModeExternal {
|
|
site.batteryModeExternal = mode
|
|
site.publish(keys.BatteryModeExternal, mode)
|
|
|
|
// start watchdog if not running
|
|
if !disable && site.batteryModeExternalTimer.IsZero() {
|
|
go func() {
|
|
for range time.Tick(time.Second) {
|
|
if site.batteryModeWatchdogExpired() {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// reset timer
|
|
if !disable {
|
|
site.batteryModeExternalTimer = time.Now()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (site *Site) batteryModeWatchdogExpired() bool {
|
|
site.RLock()
|
|
elapsed := time.Since(site.batteryModeExternalTimer)
|
|
site.RUnlock()
|
|
|
|
if elapsed > time.Minute && !site.batteryModeExternalTimer.IsZero() {
|
|
site.SetBatteryModeExternal(api.BatteryUnknown)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|