evcc-io/core/site.go

379 lines
10 KiB
Go

package core
import (
"fmt"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/core/wrapper"
"github.com/andig/evcc/push"
"github.com/andig/evcc/util"
"github.com/avast/retry-go"
)
//go:generate mockgen -package mock -destination ../mock/mock_loadpoint.go github.com/andig/evcc/core Updater
// Updater abstracts the LoadPoint implementation for testing
type Updater interface {
Update(float64)
}
// Site is the main configuration container. A site can host multiple loadpoints.
type Site struct {
uiChan chan<- util.Param // client push messages
lpUpdateChan chan *LoadPoint
log *util.Logger
// configuration
Title string `mapstructure:"title"` // UI title
Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
Meters MetersConfig // Meter references
// meters
gridMeter api.Meter // Grid usage meter
pvMeter api.Meter // PV generation meter
batteryMeter api.Meter // Battery charging meter
loadpoints []*LoadPoint // Loadpoints
// cached state
gridPower float64 // Grid power
pvPower float64 // PV power
batteryPower float64 // Battery charge power
}
// MetersConfig contains the loadpoint's meter configuration
type MetersConfig struct {
GridMeterRef string `mapstructure:"grid"` // Grid usage meter reference
PVMeterRef string `mapstructure:"pv"` // PV generation meter reference
BatteryMeterRef string `mapstructure:"battery"` // Battery charging meter reference
}
// NewSiteFromConfig creates a new site
func NewSiteFromConfig(
log *util.Logger,
cp configProvider,
other map[string]interface{},
loadpoints []*LoadPoint,
) *Site {
site := NewSite()
if err := util.DecodeOther(other, &site); err != nil {
log.FATAL.Fatal(err)
}
Voltage = site.Voltage
site.loadpoints = loadpoints
// configure meter from references
// if site.Meters.PVMeterRef == "" && site.Meters.GridMeterRef == "" {
// site.log.FATAL.Fatal("missing either pv or grid meter")
// }
if site.Meters.GridMeterRef == "" {
site.log.FATAL.Fatal("missing grid meter")
}
if site.Meters.GridMeterRef != "" {
site.gridMeter = cp.Meter(site.Meters.GridMeterRef)
}
if site.Meters.PVMeterRef != "" {
site.pvMeter = cp.Meter(site.Meters.PVMeterRef)
}
if site.Meters.BatteryMeterRef != "" {
site.batteryMeter = cp.Meter(site.Meters.BatteryMeterRef)
}
return site
}
// NewSite creates a Site with sane defaults
func NewSite() *Site {
lp := &Site{
log: util.NewLogger("core"),
Voltage: 230, // V
}
return lp
}
// SiteConfiguration contains the global site configuration
type SiteConfiguration struct {
Title string `json:"title"`
GridMeter bool `json:"gridMeter"`
PVMeter bool `json:"pvMeter"`
BatteryMeter bool `json:"batteryMeter"`
LoadPoints []LoadpointConfiguration `json:"loadpoints"`
}
// LoadpointConfiguration is the loadpoint feature structure
type LoadpointConfiguration struct {
Mode string `json:"mode"`
Title string `json:"title"`
Phases int64 `json:"phases"`
MinCurrent int64 `json:"minCurrent"`
MaxCurrent int64 `json:"maxCurrent"`
ChargeMeter bool `json:"chargeMeter"`
SoC bool `json:"soc"`
SoCCapacity int64 `json:"socCapacity"`
SoCTitle string `json:"socTitle"`
SoCLevels []int `json:"socLevels"`
TargetSoC int `json:"targetSoC"`
}
// GetMode Gets loadpoint charge mode
func (site *Site) GetMode() api.ChargeMode {
return site.loadpoints[0].GetMode()
}
// GetTargetSoC gets loadpoint charge targetSoC
func (site *Site) GetTargetSoC() int {
return site.loadpoints[0].GetTargetSoC()
}
// SetMode sets loadpoint charge mode
func (site *Site) SetMode(mode api.ChargeMode) {
site.log.INFO.Printf("set global charge mode: %s", string(mode))
for _, lp := range site.loadpoints {
lp.SetMode(mode)
}
}
// SetTargetSoC sets loadpoint charge targetSoC
func (site *Site) SetTargetSoC(targetSoC int) {
site.log.INFO.Println("set global target soc:", targetSoC)
for _, lp := range site.loadpoints {
lp.SetTargetSoC(targetSoC)
}
}
func (lp *LoadPoint) hasChargeMeter() bool {
_, isWrapped := lp.chargeMeter.(*wrapper.ChargeMeter)
return lp.chargeMeter != nil && !isWrapped
}
// LoadPoints returns the array of associated loadpoints
func (site *Site) LoadPoints() []*LoadPoint {
return site.loadpoints
}
// Configuration returns meter configuration
func (site *Site) Configuration() SiteConfiguration {
c := SiteConfiguration{
Title: site.Title,
GridMeter: site.gridMeter != nil,
PVMeter: site.pvMeter != nil,
BatteryMeter: site.batteryMeter != nil,
}
for _, lp := range site.loadpoints {
lpc := LoadpointConfiguration{
Mode: string(lp.GetMode()),
Title: lp.Name(),
Phases: lp.Phases,
MinCurrent: lp.MinCurrent,
MaxCurrent: lp.MaxCurrent,
ChargeMeter: lp.hasChargeMeter(),
}
if lp.vehicle != nil {
lpc.SoC = true
lpc.SoCCapacity = lp.vehicle.Capacity()
lpc.SoCTitle = lp.vehicle.Title()
lpc.SoCLevels = lp.SoC.Levels
lpc.TargetSoC = lp.TargetSoC
}
c.LoadPoints = append(c.LoadPoints, lpc)
}
return c
}
func logMeter(log *util.Logger, meter interface{}) {
_, power := meter.(api.Meter)
_, energy := meter.(api.MeterEnergy)
_, currents := meter.(api.MeterCurrent)
log.INFO.Printf(" power %s", presence[power])
log.INFO.Printf(" energy %s", presence[energy])
log.INFO.Printf(" currents %s", presence[currents])
}
// DumpConfig site configuration
func (site *Site) DumpConfig() {
site.log.INFO.Println("site config:")
site.log.INFO.Printf(" grid %s", presence[site.gridMeter != nil])
site.log.INFO.Printf(" pv %s", presence[site.pvMeter != nil])
site.log.INFO.Printf(" battery %s", presence[site.batteryMeter != nil])
if site.gridMeter != nil {
site.log.INFO.Println(" grid meter config:")
logMeter(site.log, site.gridMeter)
}
for i, lp := range site.loadpoints {
lp.log.INFO.Printf("loadpoint %d config:", i+1)
lp.log.INFO.Printf(" vehicle %s", presence[lp.vehicle != nil])
lp.log.INFO.Printf(" charge %s", presence[lp.hasChargeMeter()])
if lp.hasChargeMeter() {
lp.log.INFO.Println(" charge meter config:")
logMeter(site.log, lp.chargeMeter)
}
charger := lp.handler.(*ChargerHandler).charger
_, timer := charger.(api.ChargeTimer)
lp.log.INFO.Println(" charger config:")
logMeter(lp.log, charger)
lp.log.INFO.Printf(" timer %s", presence[timer])
}
}
// publish sends values to UI and databases
func (site *Site) publish(key string, val interface{}) {
// test helper
if site.uiChan == nil {
return
}
site.uiChan <- util.Param{
Key: key,
Val: val,
}
}
// updateMeter updates and publishes single meter
func (site *Site) updateMeter(name string, meter api.Meter, power *float64) error {
value, err := meter.CurrentPower()
if err != nil {
return err
}
*power = value // update value if no error
site.log.DEBUG.Printf("%s power: %.0fW", name, *power)
site.publish(name+"Power", *power)
return nil
}
// updateMeter updates and publishes single meter
func (site *Site) updateMeters() error {
retryMeter := func(s string, m api.Meter, f *float64) error {
if m == nil {
return nil
}
err := retry.Do(func() error {
return site.updateMeter(s, m, f)
}, retryOptions...)
if err != nil {
err = fmt.Errorf("updating %s meter: %v", s, err)
site.log.ERROR.Println(err)
}
return err
}
// pv meter is not critical for operation
_ = retryMeter("pv", site.pvMeter, &site.pvPower)
err := retryMeter("grid", site.gridMeter, &site.gridPower)
if err == nil {
err = retryMeter("battery", site.batteryMeter, &site.batteryPower)
}
// currents
if phaseMeter, ok := site.gridMeter.(api.MeterCurrent); err == nil && ok {
i1, i2, i3, err := phaseMeter.Currents()
if err == nil {
site.log.TRACE.Printf("grid currents: %vA", []float64{i1, i2, i3})
site.publish("gridCurrents", []float64{i1, i2, i3})
}
}
return err
}
// consumedPower estimates how much power the charger might have consumed given it was the only load
// func (site *Site) consumedPower() float64 {
// return consumedPower(lp.pvPower, lp.batteryPower, lp.gridPower)
// }
// sitePower returns the net power exported by the site minus a residual margin.
// negative values mean grid: export, battery: charging
func (site *Site) sitePower() (float64, error) {
if err := site.updateMeters(); err != nil {
return 0, err
}
sitePower := sitePower(site.gridPower, site.batteryPower, site.ResidualPower)
site.log.DEBUG.Printf("site power: %.0fW", sitePower)
return sitePower, nil
}
func (site *Site) update(lp Updater) {
site.log.DEBUG.Println("----")
if sitePower, err := site.sitePower(); err == nil {
lp.Update(sitePower)
}
}
// Prepare attaches communication channels to site and loadpoints
func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event) {
site.uiChan = uiChan
site.lpUpdateChan = make(chan *LoadPoint, 1) // 1 capacity to avoid deadlock
for id, lp := range site.loadpoints {
lpUIChan := make(chan util.Param)
lpPushChan := make(chan push.Event)
// pipe messages through go func to add id
go func(id int) {
for {
select {
case param := <-lpUIChan:
param.LoadPoint = &id
uiChan <- param
case ev := <-lpPushChan:
ev.LoadPoint = &id
pushChan <- ev
}
}
}(id)
lp.Prepare(lpUIChan, lpPushChan, site.lpUpdateChan)
}
}
// loopLoadpoints keeps iterating across loadpoints sending the next to the given channel
func (site *Site) loopLoadpoints(next chan<- Updater) {
for {
for _, lp := range site.loadpoints {
next <- lp
}
}
}
// Run is the main control loop. It reacts to trigger events by
// updating measurements and executing control logic.
func (site *Site) Run(interval time.Duration) {
loadpointChan := make(chan Updater)
go site.loopLoadpoints(loadpointChan)
ticker := time.NewTicker(interval)
site.update(<-loadpointChan) // start immediately
for {
select {
case <-ticker.C:
site.update(<-loadpointChan)
case lp := <-site.lpUpdateChan:
site.update(lp)
}
}
}