Curtailment: distribute limits to all devices by percent (#31390)
This commit is contained in:
parent
949daf4670
commit
eea55d282d
22 changed files with 147 additions and 131 deletions
|
|
@ -210,8 +210,8 @@ type Dimmer interface {
|
|||
|
||||
// Curtailer provides EEG §9 curtailment
|
||||
type Curtailer interface {
|
||||
Curtailed() (bool, error)
|
||||
Curtail(bool) error
|
||||
Curtailed() (bool, error) // curtailed if feed-in is limited to less than nominal (<100%)
|
||||
SetCurtailPercent(int) error // limit feed-in to the given percent of nominal (0..100, 100 = uncurtailed)
|
||||
}
|
||||
|
||||
// ChargeController allows to start/stop the charging session on the vehicle side
|
||||
|
|
@ -293,8 +293,8 @@ type Circuit interface {
|
|||
type HEMS interface {
|
||||
SetUpdated(func())
|
||||
Dimmed() *bool // nil = no statement
|
||||
Curtailed() *bool // nil = no statement
|
||||
MaxConsumptionPower() float64 // 0 = no limit
|
||||
CurtailedPercent() *int // allowed feed-in percent of nominal production power (0..100), nil = no statement
|
||||
MaxProductionPower() *float64 // nil = no limit
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ func (i *iCurrentGetter) GetMaxCurrent() (float64, error) {
|
|||
return i.currentGetter0()
|
||||
}
|
||||
|
||||
func Curtailer(curtailer0 func(bool) error, curtailer1 func() (bool, error)) api.Curtailer {
|
||||
func Curtailer(curtailer0 func() (bool, error), curtailer1 func(int) error) api.Curtailer {
|
||||
if curtailer0 == nil || curtailer1 == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -181,16 +181,16 @@ func Curtailer(curtailer0 func(bool) error, curtailer1 func() (bool, error)) api
|
|||
}
|
||||
|
||||
type iCurtailer struct {
|
||||
curtailer0 func(bool) error
|
||||
curtailer1 func() (bool, error)
|
||||
}
|
||||
|
||||
func (i *iCurtailer) Curtail(p0 bool) error {
|
||||
return i.curtailer0(p0)
|
||||
curtailer0 func() (bool, error)
|
||||
curtailer1 func(int) error
|
||||
}
|
||||
|
||||
func (i *iCurtailer) Curtailed() (bool, error) {
|
||||
return i.curtailer1()
|
||||
return i.curtailer0()
|
||||
}
|
||||
|
||||
func (i *iCurtailer) SetCurtailPercent(p0 int) error {
|
||||
return i.curtailer1(p0)
|
||||
}
|
||||
|
||||
func Dimmer(dimmer0 func(bool) error, dimmer1 func() (bool, error)) api.Dimmer {
|
||||
|
|
|
|||
14
api/mock.go
14
api/mock.go
|
|
@ -1212,18 +1212,18 @@ func (m *MockHEMS) EXPECT() *MockHEMSMockRecorder {
|
|||
return m.recorder
|
||||
}
|
||||
|
||||
// Curtailed mocks base method.
|
||||
func (m *MockHEMS) Curtailed() *bool {
|
||||
// CurtailedPercent mocks base method.
|
||||
func (m *MockHEMS) CurtailedPercent() *int {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Curtailed")
|
||||
ret0, _ := ret[0].(*bool)
|
||||
ret := m.ctrl.Call(m, "CurtailedPercent")
|
||||
ret0, _ := ret[0].(*int)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Curtailed indicates an expected call of Curtailed.
|
||||
func (mr *MockHEMSMockRecorder) Curtailed() *gomock.Call {
|
||||
// CurtailedPercent indicates an expected call of CurtailedPercent.
|
||||
func (mr *MockHEMSMockRecorder) CurtailedPercent() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Curtailed", reflect.TypeOf((*MockHEMS)(nil).Curtailed))
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurtailedPercent", reflect.TypeOf((*MockHEMS)(nil).CurtailedPercent))
|
||||
}
|
||||
|
||||
// Dimmed mocks base method.
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ export default defineComponent({
|
|||
: null;
|
||||
},
|
||||
feedInLimit(): string | null {
|
||||
return this.status?.curtailed && this.status.maxProductionPower !== undefined
|
||||
return (this.status?.curtailed ?? 100) < 100 &&
|
||||
this.status?.maxProductionPower !== undefined
|
||||
? this.fmtW(this.status.maxProductionPower, POWER_UNIT.KW)
|
||||
: null;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export interface HemsConfig {
|
|||
|
||||
export interface HemsStatus {
|
||||
dimmed?: boolean;
|
||||
curtailed?: boolean;
|
||||
curtailed?: number; // allowed feed-in percent (0..100 integer), <100 = curtailed
|
||||
maxConsumptionPower?: number;
|
||||
maxProductionPower?: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -837,13 +837,13 @@ export default defineComponent({
|
|||
} else if (status.dimmed !== undefined) {
|
||||
result["dimmed"] = { value: status.dimmed };
|
||||
}
|
||||
if (status.curtailed && status.maxProductionPower !== undefined) {
|
||||
if ((status.curtailed ?? 100) < 100 && status.maxProductionPower !== undefined) {
|
||||
result["curtailLimit"] = {
|
||||
value: status.maxProductionPower,
|
||||
warning: true,
|
||||
};
|
||||
} else if (status.curtailed !== undefined) {
|
||||
result["curtailed"] = { value: status.curtailed };
|
||||
result["curtailed"] = { value: status.curtailed < 100 };
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func handleCurtailFlag(cmd *cobra.Command, v any) bool {
|
|||
}
|
||||
|
||||
if vv, ok := api.Cap[api.Curtailer](v); ok {
|
||||
if err := vv.Curtail(val > 0); err != nil {
|
||||
if err := vv.SetCurtailPercent(val); err != nil {
|
||||
log.ERROR.Println("curtail:", err)
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const (
|
|||
flagDiagnoseDescription = "Diagnose"
|
||||
|
||||
flagCurtail = "curtail"
|
||||
flagCurtailDescription = "Curtail (0/1 to switch, only available if supported by device)"
|
||||
flagCurtailDescription = "Curtail feed-in to percent (0-100, only available if supported by device)"
|
||||
|
||||
flagDim = "dim"
|
||||
flagDimDescription = "Dim (0/1 to switch, only available if supported by device)"
|
||||
|
|
|
|||
|
|
@ -369,12 +369,12 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
YamlSource: yamlSource.hems,
|
||||
Status: struct {
|
||||
Dimmed *bool `json:"dimmed,omitempty"`
|
||||
Curtailed *bool `json:"curtailed,omitempty"`
|
||||
Curtailed *int `json:"curtailed,omitempty"`
|
||||
MaxConsumptionPower float64 `json:"maxConsumptionPower,omitempty"`
|
||||
MaxProductionPower *float64 `json:"maxProductionPower,omitempty"`
|
||||
}{
|
||||
Dimmed: hemsInstance.Dimmed(),
|
||||
Curtailed: hemsInstance.Curtailed(),
|
||||
Curtailed: hemsInstance.CurtailedPercent(),
|
||||
MaxConsumptionPower: hemsInstance.MaxConsumptionPower(),
|
||||
MaxProductionPower: hemsInstance.MaxProductionPower(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -86,13 +86,13 @@ func hemsDimmed(hems api.HEMS) *bool {
|
|||
return hems.Dimmed()
|
||||
}
|
||||
|
||||
// hemsCurtailed returns the HEMS curtail status, nil-safe
|
||||
func hemsCurtailed(hems api.HEMS) *bool {
|
||||
// hemsCurtailed returns the HEMS curtail percent, nil-safe
|
||||
func hemsCurtailed(hems api.HEMS) *int {
|
||||
if hems == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return hems.Curtailed()
|
||||
return hems.CurtailedPercent()
|
||||
}
|
||||
|
||||
// nonZeroEnergy reports a zero lifetime energy reading as api.ErrNotAvailable.
|
||||
|
|
|
|||
|
|
@ -89,11 +89,13 @@ func (site *Site) dimMeters(dim *bool) error {
|
|||
return errs
|
||||
}
|
||||
|
||||
func (site *Site) curtailPV(curtail *bool) error {
|
||||
if curtail == nil {
|
||||
func (site *Site) curtailPV(percent *int) error {
|
||||
if percent == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
curtail := *percent < 100
|
||||
|
||||
var errs error
|
||||
for _, dev := range site.pvMeters {
|
||||
m, ok := api.Cap[api.Curtailer](dev.Instance())
|
||||
|
|
@ -102,7 +104,7 @@ func (site *Site) curtailPV(curtail *bool) error {
|
|||
}
|
||||
|
||||
if curtailed, err := backoff.RetryWithData(m.Curtailed, modbus.Backoff()); err == nil {
|
||||
if *curtail == curtailed {
|
||||
if curtail == curtailed {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
|
|
@ -112,8 +114,8 @@ func (site *Site) curtailPV(curtail *bool) error {
|
|||
continue
|
||||
}
|
||||
|
||||
if err := m.Curtail(*curtail); err == nil {
|
||||
site.log.DEBUG.Printf("%s curtail: %t", deviceTitleOrName(dev), *curtail)
|
||||
if err := m.SetCurtailPercent(*percent); err == nil {
|
||||
site.log.DEBUG.Printf("%s curtail: %d%%", deviceTitleOrName(dev), *percent)
|
||||
} else if !errors.Is(err, api.ErrNotAvailable) {
|
||||
errs = errors.Join(errs, fmt.Errorf("%s curtail: %w", deviceTitleOrName(dev), err))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,9 +41,10 @@ type EEBus struct {
|
|||
failsafeConsumptionLimit float64
|
||||
|
||||
smartgridProductionId uint
|
||||
productionLimit ucapi.LoadLimit
|
||||
productionLimit ucapi.LoadLimit // feed-in limit (NOT production despite its name)
|
||||
productionLimitActivated time.Time
|
||||
failsafeProductionLimit *float64
|
||||
failsafeProductionLimit *float64 // feed-in limit (NOT production despite its name)
|
||||
productionNominalMax float64
|
||||
|
||||
heartbeat *util.Value[struct{}]
|
||||
interval time.Duration
|
||||
|
|
@ -113,6 +114,7 @@ func NewEEBus(ctx context.Context, ski string, limits Limits, passthrough func(b
|
|||
failsafeDuration: limits.FailsafeDurationMinimum,
|
||||
failsafeConsumptionLimit: limits.FailsafeConsumptionActivePowerLimit,
|
||||
failsafeProductionLimit: limits.FailsafeProductionActivePowerLimit,
|
||||
productionNominalMax: limits.ProductionNominalMax,
|
||||
}
|
||||
|
||||
// simulate a received heartbeat
|
||||
|
|
@ -308,11 +310,24 @@ func (c *EEBus) Dimmed() *bool {
|
|||
return new(!c.consumptionLimitActivated.IsZero())
|
||||
}
|
||||
|
||||
// Curtailed implements api.HEMS, derived from productionLimitActivated.
|
||||
func (c *EEBus) Curtailed() *bool {
|
||||
// CurtailedPercent implements api.HEMS, converting the active LPP production
|
||||
// limit to an allowed production percent via the configured nominal production power.
|
||||
func (c *EEBus) CurtailedPercent() *int {
|
||||
c.mux.RLock()
|
||||
defer c.mux.RUnlock()
|
||||
return new(!c.productionLimitActivated.IsZero())
|
||||
|
||||
// without a nominal reference the W limit cannot be expressed as a percent
|
||||
if c.productionNominalMax <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
percent := 100
|
||||
if !c.productionLimitActivated.IsZero() {
|
||||
// production limits are negative watts
|
||||
percent = int(-c.productionLimit.Value / c.productionNominalMax * 100)
|
||||
}
|
||||
|
||||
return &percent
|
||||
}
|
||||
|
||||
// MaxConsumptionPower implements api.HEMS, returning the consumption cap
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const (
|
|||
testFailsafeConsumption = 4200.0
|
||||
testFailsafeProduction = 1000.0
|
||||
testFailsafeDuration = 2 * time.Hour
|
||||
testProductionNominal = 2000.0
|
||||
)
|
||||
|
||||
// stubSite implements site.API for testing — only GetGridPower is exercised;
|
||||
|
|
@ -43,6 +44,7 @@ func newTestEEBus(t *testing.T) *EEBus {
|
|||
failsafeConsumptionLimit: testFailsafeConsumption,
|
||||
failsafeProductionLimit: &failsafeProduction,
|
||||
failsafeDuration: testFailsafeDuration,
|
||||
productionNominalMax: testProductionNominal,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +58,9 @@ func assertConsumptionLimit(t *testing.T, c *EEBus, limit float64) {
|
|||
// assertProductionLimit checks the HEMS production state through the api.HEMS surface.
|
||||
func assertProductionLimit(t *testing.T, c *EEBus, active bool) {
|
||||
t.Helper()
|
||||
assert.Equal(t, new(active), c.Curtailed())
|
||||
percent := c.CurtailedPercent()
|
||||
require.NotNil(t, percent)
|
||||
assert.Equal(t, active, *percent < 100)
|
||||
}
|
||||
|
||||
// TestRun_HeartbeatLost_EntersFailsafe verifies the LPC-911/LPP-911 transition:
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ func NewFnn(site site.API, maxDimPower, maxCurtailPower float64, w3G, s1G, s2G,
|
|||
s2: s2G,
|
||||
w3: w3G,
|
||||
w4: w4G,
|
||||
productionPercent: 100,
|
||||
interval: interval,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -107,7 +108,7 @@ type Fnn struct {
|
|||
smartgridProductionID uint
|
||||
|
||||
consumptionLimit float64
|
||||
productionLimit *float64
|
||||
productionPercent int // allowed feed-in percent (0..100), 100 = uncurtailed
|
||||
|
||||
interval time.Duration
|
||||
}
|
||||
|
|
@ -145,11 +146,11 @@ func (c *Fnn) runCurtail() error {
|
|||
|
||||
rules := []struct {
|
||||
get func() (bool, error)
|
||||
frac float64
|
||||
percent int
|
||||
}{
|
||||
{get: c.w3, frac: 0.0},
|
||||
{get: c.s2, frac: 0.3},
|
||||
{get: c.s1, frac: 0.6},
|
||||
{get: c.w3, percent: 0},
|
||||
{get: c.s2, percent: 30},
|
||||
{get: c.s1, percent: 60},
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
|
|
@ -163,12 +164,12 @@ func (c *Fnn) runCurtail() error {
|
|||
}
|
||||
|
||||
if active {
|
||||
return c.setProductionLimit(rule.frac)
|
||||
return c.setProductionLimit(rule.percent)
|
||||
}
|
||||
}
|
||||
|
||||
// 100%
|
||||
return c.setProductionLimit(1.0)
|
||||
return c.setProductionLimit(100)
|
||||
}
|
||||
|
||||
// runDim evaluates the dimming rule and applies the dim limit.
|
||||
|
|
@ -192,20 +193,16 @@ func (c *Fnn) runDim() error {
|
|||
}
|
||||
|
||||
// setProductionLimit applies the curtailment limit.
|
||||
func (c *Fnn) setProductionLimit(frac float64) error {
|
||||
func (c *Fnn) setProductionLimit(percent int) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
active := frac < 1.0
|
||||
|
||||
c.productionLimit = nil
|
||||
if active {
|
||||
c.productionLimit = new(c.maxCurtailPower * frac)
|
||||
}
|
||||
active := percent < 100
|
||||
c.productionPercent = percent
|
||||
|
||||
limit := 0.0
|
||||
if c.productionLimit != nil {
|
||||
limit = *c.productionLimit
|
||||
if active {
|
||||
limit = float64(percent) / 100 * c.maxCurtailPower
|
||||
}
|
||||
|
||||
if err := smartgrid.UpdateSession(&c.smartgridProductionID, smartgrid.Curtail, c.site.GetGridPower(), limit, active); err != nil {
|
||||
|
|
@ -243,15 +240,17 @@ func (c *Fnn) Dimmed() *bool {
|
|||
return new(c.consumptionLimit > 0)
|
||||
}
|
||||
|
||||
// Curtailed implements api.HEMS.
|
||||
func (c *Fnn) Curtailed() *bool {
|
||||
// CurtailedPercent implements api.HEMS, returning the allowed production percent.
|
||||
func (c *Fnn) CurtailedPercent() *int {
|
||||
if c.w3 == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return new(c.productionLimit != nil)
|
||||
|
||||
percent := c.productionPercent
|
||||
return &percent
|
||||
}
|
||||
|
||||
// MaxConsumptionPower implements api.HEMS.
|
||||
|
|
@ -265,9 +264,9 @@ func (c *Fnn) MaxConsumptionPower() float64 {
|
|||
func (c *Fnn) MaxProductionPower() *float64 {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.productionLimit == nil {
|
||||
if c.productionPercent >= 100 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return new(*c.productionLimit)
|
||||
return new(float64(c.productionPercent) / 100 * c.maxCurtailPower)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ func TestFnn(t *testing.T) {
|
|||
return true, nil
|
||||
}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, fnn.Curtailed())
|
||||
require.Nil(t, fnn.CurtailedPercent())
|
||||
// require.NoError(t, fnn.runDim())
|
||||
// require.Equal(t, new(true), fnn.Dimmed())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,9 +149,9 @@ func (c *Relay) Dimmed() *bool {
|
|||
return new(c.limit != nil)
|
||||
}
|
||||
|
||||
// Curtailed implements api.HEMS. Relay does not curtail production and
|
||||
// CurtailedPercent implements api.HEMS. Relay does not curtail production and
|
||||
// hence makes no statement.
|
||||
func (c *Relay) Curtailed() *bool {
|
||||
func (c *Relay) CurtailedPercent() *int {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -278,18 +278,9 @@ func (c *EEBus) Curtailed() (bool, error) {
|
|||
return limit.IsActive && limit.Value <= 0, nil
|
||||
}
|
||||
|
||||
// Curtail implements the api.Curtailer interface
|
||||
func (c *EEBus) Curtail(curtail bool) error {
|
||||
// Sets or removes the production power limit
|
||||
|
||||
// TODO: change api.Curtailer to make limit configurable
|
||||
// For now, we use a fixed safe limit of 0W
|
||||
limit := 0.0
|
||||
|
||||
var value float64
|
||||
if curtail {
|
||||
value = limit
|
||||
}
|
||||
// SetCurtailPercent implements the api.Curtailer interface
|
||||
func (c *EEBus) SetCurtailPercent(percent int) error {
|
||||
curtail := percent < 100
|
||||
|
||||
c.mu.Lock()
|
||||
entity := c.egLppEntity
|
||||
|
|
@ -299,6 +290,15 @@ func (c *EEBus) Curtail(curtail bool) error {
|
|||
return api.ErrNotAvailable
|
||||
}
|
||||
|
||||
// derive a proportional feed-in limit from the producer's nominal power
|
||||
// (limits are negative watts); fall back to a safe 0W limit if unavailable
|
||||
var value float64
|
||||
if curtail {
|
||||
if nominal, err := c.eg.EgLPPInterface.ProductionNominalMax(entity); err == nil && nominal > 0 {
|
||||
value = -float64(percent) / 100 * nominal
|
||||
}
|
||||
}
|
||||
|
||||
return eebus.Await(func(cb func(model.ResultDataType)) (*model.MsgCounterType, error) {
|
||||
return c.eg.EgLPPInterface.WriteProductionLimit(entity, ucapi.LoadLimit{Value: value, IsActive: curtail}, cb)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func (c *EEBus) maUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) {
|
|||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// use most specific selector
|
||||
// prefer the shallowest (device-level) entity
|
||||
if c.maEntity == nil || len(entity.Address().Entity) < len(c.maEntity.Address().Entity) {
|
||||
c.maEntity = entity
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ func (c *EEBus) egLpcUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface)
|
|||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// use most specific selector
|
||||
// prefer the shallowest (device-level) entity
|
||||
if c.egLpcEntity == nil || len(entity.Address().Entity) < len(c.egLpcEntity.Address().Entity) {
|
||||
c.egLpcEntity = entity
|
||||
}
|
||||
|
|
@ -90,7 +90,7 @@ func (c *EEBus) egLppUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface)
|
|||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// use most specific selector
|
||||
// prefer the shallowest (device-level) entity
|
||||
if c.egLppEntity == nil || len(entity.Address().Entity) < len(c.egLppEntity.Address().Entity) {
|
||||
c.egLppEntity = entity
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,11 +53,11 @@ type Curtailer struct {
|
|||
}
|
||||
|
||||
func (cc *Curtailer) Configure(ctx context.Context) (
|
||||
func(bool) error,
|
||||
func(int64) error,
|
||||
func() (bool, error),
|
||||
error,
|
||||
) {
|
||||
curtailS, err := cc.Curtail.BoolSetter(ctx, "curtail")
|
||||
curtailS, err := cc.Curtail.IntSetter(ctx, "curtail")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("curtail: %w", err)
|
||||
}
|
||||
|
|
@ -80,7 +80,10 @@ func (cc *Curtailer) Implement(ctx context.Context, i implement.Caps) error {
|
|||
return err
|
||||
}
|
||||
|
||||
implement.May(i, implement.Curtailer(curtailS, curtailedG))
|
||||
if curtailS != nil {
|
||||
setPercent := func(percent int) error { return curtailS(int64(percent)) }
|
||||
implement.May(i, implement.Curtailer(curtailedG, setPercent))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
10
meter/rct.go
10
meter/rct.go
|
|
@ -107,12 +107,8 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL
|
|||
}
|
||||
|
||||
if usage == "pv" {
|
||||
curtail := func(b bool) error {
|
||||
var r float64
|
||||
if !b {
|
||||
r = 1.0
|
||||
}
|
||||
return m.conn.Write(rct.BufVControlPowerReduction, floatVal(r))
|
||||
curtail := func(percent int) error {
|
||||
return m.conn.Write(rct.BufVControlPowerReduction, floatVal(float64(percent)/100))
|
||||
}
|
||||
|
||||
curtailed := func() (bool, error) {
|
||||
|
|
@ -120,7 +116,7 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL
|
|||
return r != 1, err
|
||||
}
|
||||
|
||||
implement.Has(m, implement.Curtailer(curtail, curtailed))
|
||||
implement.Has(m, implement.Curtailer(curtailed, curtail))
|
||||
implement.May(m, implement.MaxACPowerGetter(maxACPower))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ render: |
|
|||
source: js
|
||||
vm: shared
|
||||
script: |
|
||||
typeof demoMeterCurtailed !== "undefined" && demoMeterCurtailed
|
||||
typeof demoMeterCurtailed !== "undefined" && demoMeterCurtailed < 100
|
||||
{{- end }}
|
||||
{{- if .energy }}
|
||||
energy:
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ params:
|
|||
en: Active power limit in percent
|
||||
advanced: true
|
||||
usages: ["pv"]
|
||||
deprecated: true # curtailment now applies the HEMS-distributed percent directly
|
||||
- name: storageunit
|
||||
- name: forceaccharging
|
||||
default: false
|
||||
|
|
@ -226,38 +227,33 @@ render: |
|
|||
scale: 0.01
|
||||
maxacpower: {{ .maxacpower }}
|
||||
curtail:
|
||||
source: ifelse
|
||||
if:
|
||||
source: convert
|
||||
convert: bool2int
|
||||
set:
|
||||
source: sequence
|
||||
set:
|
||||
- source: const
|
||||
value: {{ .curtailpercent }}
|
||||
set:
|
||||
source: modbus
|
||||
{{- include "modbus" . | indent 10 }}
|
||||
# allowed feed-in power as percent of nominal (0-100 %)
|
||||
- source: modbus
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
register:
|
||||
address: 47418 # Maximum Feed Grid Power (0-100 %)
|
||||
type: writesingle
|
||||
encoding: uint16
|
||||
scale: 10
|
||||
- source: const
|
||||
value: 7 # Power-limited grid connection (%)
|
||||
# active power control mode: 7 = percentage limit while curtailed, 0 = unlimited at 100%
|
||||
- source: switch
|
||||
switch:
|
||||
- case: 100 # uncurtailed
|
||||
set:
|
||||
source: const
|
||||
value: 0 # Unlimited
|
||||
set:
|
||||
source: modbus
|
||||
{{- include "modbus" . | indent 10 }}
|
||||
{{- include "modbus" . | indent 12 }}
|
||||
register:
|
||||
address: 47415 # Active power control mode
|
||||
type: writesingle
|
||||
encoding: uint16
|
||||
else:
|
||||
source: convert
|
||||
convert: bool2int
|
||||
set:
|
||||
default:
|
||||
source: const
|
||||
value: 0 # Unlimited
|
||||
value: 7 # Power-limited grid connection (%)
|
||||
set:
|
||||
source: modbus
|
||||
{{- include "modbus" . | indent 8 }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue