410 lines
10 KiB
Go
410 lines
10 KiB
Go
package circuit
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cenkalti/backoff/v4"
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/plugin"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/config"
|
|
"github.com/evcc-io/evcc/util/modbus"
|
|
)
|
|
|
|
var _ api.Circuit = (*Circuit)(nil)
|
|
|
|
// the circuit instances to control the load
|
|
type Circuit struct {
|
|
mu sync.RWMutex
|
|
log *util.Logger
|
|
|
|
title string
|
|
parent api.Circuit // parent circuit
|
|
children []api.Circuit // child circuits
|
|
meter api.Meter // meter to determine current power
|
|
timeout time.Duration
|
|
|
|
maxCurrent float64 // max allowed current
|
|
maxPower float64 // max allowed power
|
|
getMaxCurrent func() (float64, error) // dynamic max allowed current
|
|
getMaxPower func() (float64, error) // dynamic max allowed power
|
|
|
|
current float64
|
|
power float64
|
|
|
|
hems api.HEMS // only set on the root circuit, supplies the HEMS consumption cap
|
|
|
|
currentUpdated time.Time
|
|
powerUpdated time.Time
|
|
}
|
|
|
|
func init() {
|
|
registry.AddCtx(api.Custom, NewConfigurableFromConfig)
|
|
}
|
|
|
|
// NewConfigurableFromConfig creates a new circuit from config
|
|
func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.Circuit, error) {
|
|
cc := struct {
|
|
Title string // title
|
|
ParentRef string `mapstructure:"parent"` // parent circuit reference
|
|
MeterRef string `mapstructure:"meter"` // meter reference
|
|
MaxCurrent float64 // the max allowed current
|
|
MaxPower float64 // the max allowed power
|
|
GetMaxCurrent *plugin.Config // dynamic max allowed current
|
|
GetMaxPower *plugin.Config // dynamic max allowed power
|
|
Timeout time.Duration // timeout between meter updates
|
|
}{
|
|
Timeout: time.Minute,
|
|
}
|
|
|
|
// drop circuit type- all circuits are custom
|
|
delete(other, "type")
|
|
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var meter api.Meter
|
|
if cc.MeterRef != "" {
|
|
dev, err := config.Meters().ByName(cc.MeterRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
meter = dev.Instance()
|
|
if meter == nil {
|
|
return nil, errors.New("missing meter instance")
|
|
}
|
|
}
|
|
|
|
log := util.ContextLoggerWithDefault(ctx, util.NewLogger("circuit"))
|
|
circuit, err := New(log, cc.Title, cc.MaxCurrent, cc.MaxPower, meter, cc.Timeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
circuit.getMaxPower, err = cc.GetMaxPower.FloatGetter(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
circuit.getMaxCurrent, err = cc.GetMaxCurrent.FloatGetter(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if cc.ParentRef != "" {
|
|
dev, err := config.Circuits().ByName(cc.ParentRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parent := dev.Instance()
|
|
if parent == nil {
|
|
return nil, fmt.Errorf("missing parent circuit instance: %s", cc.ParentRef)
|
|
}
|
|
circuit.setParent(parent)
|
|
}
|
|
|
|
return circuit, err
|
|
}
|
|
|
|
// New creates a circuit
|
|
func New(log *util.Logger, title string, maxCurrent, maxPower float64, meter api.Meter, timeout time.Duration) (*Circuit, error) {
|
|
c := &Circuit{
|
|
log: log,
|
|
title: title,
|
|
maxCurrent: maxCurrent,
|
|
maxPower: maxPower,
|
|
meter: meter,
|
|
timeout: timeout,
|
|
}
|
|
|
|
if maxPower == 0 {
|
|
c.log.DEBUG.Printf("validation of max power disabled")
|
|
}
|
|
|
|
if maxCurrent == 0 {
|
|
c.log.DEBUG.Printf("validation of max phase current disabled")
|
|
} else if meter != nil && !api.HasCap[api.PhaseCurrents](meter) {
|
|
return nil, errors.New("meter does not support phase currents")
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func (c *Circuit) GetTitle() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.title
|
|
}
|
|
|
|
func (c *Circuit) SetTitle(title string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.title = title
|
|
}
|
|
|
|
// GetParent returns the parent circuit
|
|
func (c *Circuit) GetParent() api.Circuit {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.parent
|
|
}
|
|
|
|
// setParent set parent circuit
|
|
func (c *Circuit) setParent(parent api.Circuit) error {
|
|
// prevent cyclical dependency
|
|
for p := parent.GetParent(); p != nil; p = p.GetParent() {
|
|
if c == p {
|
|
return fmt.Errorf("cycle detected: %s and %s cannot be mutual parents", c.GetTitle(), parent.GetTitle())
|
|
}
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.parent != nil {
|
|
return errors.New("circuit already has a parent")
|
|
}
|
|
c.parent = parent
|
|
if parent != nil {
|
|
parent.RegisterChild(c)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetHEMS attaches the HEMS instance to the circuit. Valid on root circuit only.
|
|
func (c *Circuit) SetHEMS(hems api.HEMS) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.hems = hems
|
|
}
|
|
|
|
// HasMeter returns the max power setting
|
|
func (c *Circuit) HasMeter() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.meter != nil
|
|
}
|
|
|
|
// GetMaxPower returns the configured max power setting.
|
|
func (c *Circuit) GetMaxPower() float64 {
|
|
if c.getMaxPower != nil {
|
|
res, err := c.getMaxPower()
|
|
if err == nil {
|
|
return res
|
|
}
|
|
c.log.WARN.Printf("get max power: %v", err)
|
|
}
|
|
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.maxPower
|
|
}
|
|
|
|
// effectiveMaxPower returns the configured max power clamped by the HEMS
|
|
// consumption limit. Only the root circuit applies the HEMS clamp; non-root
|
|
// callers walk up the parent chain via ValidatePower.
|
|
func (c *Circuit) effectiveMaxPower() float64 {
|
|
maxPower := c.GetMaxPower()
|
|
if c.hems == nil {
|
|
return maxPower
|
|
}
|
|
|
|
hemsLimit := c.hems.MaxConsumptionPower()
|
|
if hemsLimit == nil || *hemsLimit <= 0 {
|
|
return maxPower
|
|
}
|
|
|
|
// unconfigured maxPower means unlimited, so the HEMS limit applies alone
|
|
if maxPower <= 0 {
|
|
return *hemsLimit
|
|
}
|
|
|
|
return min(*hemsLimit, maxPower)
|
|
}
|
|
|
|
// SetMaxPower sets the max power
|
|
func (c *Circuit) SetMaxPower(power float64) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.maxPower = power
|
|
}
|
|
|
|
// GetMaxCurrent returns the max current setting
|
|
func (c *Circuit) GetMaxCurrent() float64 {
|
|
if c.getMaxCurrent != nil {
|
|
res, err := c.getMaxCurrent()
|
|
if err == nil {
|
|
return res
|
|
}
|
|
c.log.WARN.Printf("get max current: %v", err)
|
|
}
|
|
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.maxCurrent
|
|
}
|
|
|
|
// SetMaxCurrent sets the max current
|
|
func (c *Circuit) SetMaxCurrent(current float64) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.maxCurrent = current
|
|
}
|
|
|
|
// RegisterChild registers child circuit
|
|
func (c *Circuit) RegisterChild(child api.Circuit) {
|
|
c.children = append(c.children, child)
|
|
}
|
|
|
|
func (c *Circuit) updateLoadpoints(loadpoints []api.CircuitLoad) {
|
|
c.power = 0
|
|
c.current = 0
|
|
|
|
for _, lp := range loadpoints {
|
|
if lp.GetCircuit() != c {
|
|
continue
|
|
}
|
|
|
|
c.power += lp.GetChargePower()
|
|
c.current += lp.GetMaxPhaseCurrent()
|
|
}
|
|
}
|
|
|
|
func (c *Circuit) overloadOnError(t time.Time, val *float64) {
|
|
if c.timeout > 0 && time.Since(t) > c.timeout {
|
|
*val = math.MaxFloat64
|
|
}
|
|
}
|
|
|
|
func (c *Circuit) updateMeters() error {
|
|
if f, err := backoff.RetryWithData(c.meter.CurrentPower, modbus.Backoff()); err == nil {
|
|
c.power = f
|
|
c.powerUpdated = time.Now()
|
|
} else {
|
|
c.overloadOnError(c.powerUpdated, &c.power)
|
|
return fmt.Errorf("circuit power: %w", err)
|
|
}
|
|
|
|
if phaseMeter, ok := api.Cap[api.PhaseCurrents](c.meter); ok {
|
|
var i1, i2, i3 float64
|
|
if err := backoff.Retry(func() error {
|
|
var err error
|
|
i1, i2, i3, err = phaseMeter.Currents()
|
|
return err
|
|
}, modbus.Backoff()); err != nil {
|
|
c.overloadOnError(c.currentUpdated, &c.current)
|
|
return fmt.Errorf("circuit currents: %w", err)
|
|
}
|
|
|
|
var p1, p2, p3 float64
|
|
if phaseMeter, ok := api.Cap[api.PhasePowers](c.meter); ok {
|
|
var err error // phases needed for signed currents
|
|
if p1, p2, p3, err = phaseMeter.Powers(); err != nil {
|
|
return fmt.Errorf("circuit powers: %w", err)
|
|
}
|
|
}
|
|
|
|
c.current = max(util.SignFromPower(i1, p1), util.SignFromPower(i2, p2), util.SignFromPower(i3, p3))
|
|
c.currentUpdated = time.Now()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Circuit) Update(loadpoints []api.CircuitLoad) (err error) {
|
|
maxPower := c.GetMaxPower()
|
|
maxCurrent := c.GetMaxCurrent()
|
|
|
|
defer func() {
|
|
if maxPower != 0 && c.power > maxPower {
|
|
c.log.WARN.Printf("over power detected: %.0fW > %.0fW", c.power, maxPower)
|
|
} else {
|
|
c.log.DEBUG.Printf("power: %.0fW", c.power)
|
|
}
|
|
|
|
if maxCurrent != 0 && c.current > maxCurrent {
|
|
c.log.WARN.Printf("over current detected: %.3gA > %.3gA", c.current, maxCurrent)
|
|
} else {
|
|
c.log.DEBUG.Printf("current: %.3gA", c.current)
|
|
}
|
|
}()
|
|
|
|
// update children depth-first
|
|
for _, ch := range c.children {
|
|
if err := ch.Update(loadpoints); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// meter available
|
|
if c.meter != nil {
|
|
return c.updateMeters()
|
|
}
|
|
|
|
// no meter available
|
|
c.updateLoadpoints(loadpoints)
|
|
for _, ch := range c.children {
|
|
c.power += ch.GetChargePower()
|
|
c.current += ch.GetMaxPhaseCurrent()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetChargePower returns the actual power
|
|
func (c *Circuit) GetChargePower() float64 {
|
|
return c.power
|
|
}
|
|
|
|
// GetMaxPhaseCurrent returns the actual current
|
|
func (c *Circuit) GetMaxPhaseCurrent() float64 {
|
|
return c.current
|
|
}
|
|
|
|
// ValidatePower validates power request
|
|
func (c *Circuit) ValidatePower(old, new float64) float64 {
|
|
if maxPower := c.effectiveMaxPower(); maxPower != 0 {
|
|
delta := max(0, new-old)
|
|
potential := maxPower - c.power
|
|
|
|
if delta > potential {
|
|
capped := min(new, max(0, old+potential))
|
|
c.log.DEBUG.Printf("validate power: %.0fW + (%.0fW -> %.0fW) > %.0fW capped at %.0fW", c.power, old, new, maxPower, capped)
|
|
new = capped
|
|
} else {
|
|
c.log.TRACE.Printf("validate power: %.0fW + (%.0fW -> %.0fW) <= %.0fW ok", c.power, old, new, maxPower)
|
|
}
|
|
}
|
|
|
|
if c.parent == nil {
|
|
return new
|
|
}
|
|
|
|
return c.parent.ValidatePower(old, new)
|
|
}
|
|
|
|
// ValidateCurrent validates current request
|
|
func (c *Circuit) ValidateCurrent(old, new float64) float64 {
|
|
if maxCurrent := c.GetMaxCurrent(); maxCurrent != 0 {
|
|
delta := max(0, new-old)
|
|
potential := maxCurrent - c.current
|
|
|
|
if delta > potential {
|
|
capped := min(new, max(0, old+potential))
|
|
c.log.DEBUG.Printf("validate current: %.3gA + (%.3gA -> %.3gA) > %.3gA capped at %.3gA", c.current, old, new, maxCurrent, capped)
|
|
new = capped
|
|
} else {
|
|
c.log.TRACE.Printf("validate current: %.3gA + (%.3gA -> %.3gA) <= %.3gA ok", c.current, old, new, maxCurrent)
|
|
}
|
|
}
|
|
|
|
if c.parent == nil {
|
|
return new
|
|
}
|
|
|
|
return c.parent.ValidateCurrent(old, new)
|
|
}
|