package core import ( "errors" "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), ",")) } // 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 } // 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 } // 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 } 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 } // 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 }