199 lines
4.9 KiB
Go
199 lines
4.9 KiB
Go
package core
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"sync"
|
|
|
|
"github.com/cenkalti/backoff/v4"
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/core/keys"
|
|
"github.com/evcc-io/evcc/hems/hems"
|
|
"github.com/evcc-io/evcc/util/config"
|
|
"github.com/evcc-io/evcc/util/modbus"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type circuitStruct struct {
|
|
Title string `json:"title,omitempty"`
|
|
Icon string `json:"icon,omitempty"`
|
|
Parent string `json:"parent,omitempty"`
|
|
Power float64 `json:"power"`
|
|
Current *float64 `json:"current,omitempty"`
|
|
MaxPower float64 `json:"maxPower,omitempty"`
|
|
MaxCurrent float64 `json:"maxCurrent,omitempty"`
|
|
}
|
|
|
|
// updateCircuits updates all circuits' power and currents
|
|
func (site *Site) updateCircuits() {
|
|
if site.circuit == nil {
|
|
return
|
|
}
|
|
|
|
if err := site.circuit.Update(site.loadpointsAsCircuitDevices()); err != nil {
|
|
site.log.ERROR.Println(err)
|
|
}
|
|
|
|
site.publishCircuits()
|
|
}
|
|
|
|
// applyHemsLimits applies the HEMS dim and curtail state to the site's devices
|
|
func (site *Site) applyHemsLimits() {
|
|
if site.hems == nil {
|
|
return
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
wg.Go(func() {
|
|
if dim := hems.Dimmed(site.hems); dim != nil {
|
|
if err := site.dimMeters(*dim); err != nil {
|
|
site.log.ERROR.Println(err)
|
|
}
|
|
}
|
|
})
|
|
|
|
wg.Go(func() {
|
|
if hems.Curtailed(site.hems) != nil {
|
|
if err := site.curtailPV(site.hems.CurtailedPercent()); err != nil {
|
|
site.log.ERROR.Println(err)
|
|
}
|
|
}
|
|
})
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// publishCircuits returns a list of circuit titles
|
|
func (site *Site) publishCircuits() {
|
|
cc := config.Circuits().Devices()
|
|
res := make(map[string]circuitStruct, len(cc))
|
|
|
|
names := make(map[api.Circuit]string, len(cc))
|
|
for _, c := range cc {
|
|
names[c.Instance()] = c.Config().Name
|
|
}
|
|
|
|
for _, c := range cc {
|
|
instance := c.Instance()
|
|
props := deviceProperties(c)
|
|
|
|
data := circuitStruct{
|
|
Title: instance.GetTitle(),
|
|
Icon: props.Icon,
|
|
Parent: names[instance.GetParent()],
|
|
Power: instance.GetChargePower(),
|
|
MaxPower: instance.GetMaxPower(),
|
|
MaxCurrent: instance.GetMaxCurrent(),
|
|
}
|
|
|
|
if instance.GetMaxCurrent() > 0 {
|
|
data.Current = lo.EmptyableToPtr(instance.GetMaxPhaseCurrent())
|
|
}
|
|
|
|
res[c.Config().Name] = data
|
|
}
|
|
|
|
site.publish(keys.Circuits, res)
|
|
}
|
|
|
|
// dimMeters applies the HEMS dim state to all dimmable aux and ext meters.
|
|
// Devices are only queried when the state changes or after a failed attempt.
|
|
func (site *Site) dimMeters(dim bool) error {
|
|
if site.dimmed != nil && *site.dimmed == dim {
|
|
return nil
|
|
}
|
|
|
|
// invalidate until successfully applied
|
|
site.dimmed = nil
|
|
|
|
var errs error
|
|
for _, dev := range slices.Concat(site.auxMeters, site.extMeters) {
|
|
m, ok := api.Cap[api.Dimmer](dev.Instance())
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// unreadable state: apply unconditionally
|
|
dimmed, err := backoff.RetryWithData(m.Dimmed, modbus.Backoff())
|
|
if err != nil && !errors.Is(err, api.ErrNotAvailable) {
|
|
errs = errors.Join(errs, fmt.Errorf("%s dimmed: %w", deviceTitleOrName(dev), err))
|
|
continue
|
|
}
|
|
if err == nil && dim == dimmed {
|
|
continue
|
|
}
|
|
|
|
if err := m.Dim(dim); err == nil {
|
|
site.log.DEBUG.Printf("%s dim: %t", deviceTitleOrName(dev), dim)
|
|
} else if !errors.Is(err, api.ErrNotAvailable) {
|
|
errs = errors.Join(errs, fmt.Errorf("%s dim: %w", deviceTitleOrName(dev), err))
|
|
}
|
|
}
|
|
|
|
if errs == nil {
|
|
site.dimmed = &dim
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// curtailable is a named curtailment device
|
|
type curtailable struct {
|
|
name string
|
|
api.Curtailer
|
|
}
|
|
|
|
// curtailables returns the curtailment devices and the curtailable pv meters
|
|
func (site *Site) curtailables() []curtailable {
|
|
var res []curtailable
|
|
|
|
for _, dev := range site.pvMeters {
|
|
if m, ok := api.Cap[api.Curtailer](dev.Instance()); ok {
|
|
res = append(res, curtailable{deviceTitleOrName(dev), m})
|
|
}
|
|
}
|
|
|
|
for _, dev := range site.curtailers {
|
|
res = append(res, curtailable{deviceTitleOrName(dev), dev.Instance()})
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// curtailPV applies the HEMS curtailment percent to all curtailment devices and curtailable pv meters.
|
|
// Devices are only queried when the percent changes or after a failed attempt.
|
|
func (site *Site) curtailPV(percent *int) error {
|
|
if percent == nil || site.curtailPercent != nil && *site.curtailPercent == *percent {
|
|
return nil
|
|
}
|
|
|
|
// invalidate until successfully applied
|
|
site.curtailPercent = nil
|
|
|
|
var errs error
|
|
for _, m := range site.curtailables() {
|
|
// unreadable state: apply unconditionally
|
|
curtailed, err := backoff.RetryWithData(m.CurtailedPercent, modbus.Backoff())
|
|
if err != nil && !errors.Is(err, api.ErrNotAvailable) {
|
|
errs = errors.Join(errs, fmt.Errorf("%s curtailed: %w", m.name, err))
|
|
continue
|
|
}
|
|
if err == nil && curtailed == *percent {
|
|
continue
|
|
}
|
|
|
|
if err := m.SetCurtailPercent(*percent); err == nil {
|
|
site.log.DEBUG.Printf("%s curtail: %d%%", m.name, *percent)
|
|
} else if !errors.Is(err, api.ErrNotAvailable) {
|
|
errs = errors.Join(errs, fmt.Errorf("%s curtail: %w", m.name, err))
|
|
}
|
|
}
|
|
|
|
if errs == nil {
|
|
site.curtailPercent = new(*percent)
|
|
}
|
|
|
|
return errs
|
|
}
|