Skip to content

Curtailment: report curtailed percent instead of bool - #32010

Merged
andig merged 4 commits into
masterfrom
feat/curtailed-percent
Jul 21, 2026
Merged

Curtailment: report curtailed percent instead of bool#32010
andig merged 4 commits into
masterfrom
feat/curtailed-percent

Conversation

@andig

@andig andig commented Jul 21, 2026

Copy link
Copy Markdown
Member

pairs with #32006

api.Curtailer reported curtailment as a bool, which cannot express which limit a device currently applies. As a result curtailPV compares "is curtailed" against "should be curtailed", so a HEMS moving between two curtailed steps (FNN emits 0/30/60/100, EEBus derives arbitrary percents from watts) is skipped and the new limit never reaches the device. Reporting the percent makes the comparison meaningful and mirrors SetCurtailPercent and api.HEMS.CurtailedPercent.

  • Curtailed() (bool, error) becomes CurtailedPercent() (int, error), 100 = uncurtailed
  • a device already at the requested percent is still skipped, any other percent is now written
  • the curtailed plugin config changes from a bool to a percent, all curtailable meter templates read the limit register they already write
  • EEBus reports ErrNotAvailable when the nominal production power needed to express the watt limit as a percent is unknown, instead of treating any active limit as curtailed

Custom configurations with a hand-written curtailed plugin need to return the percent instead of a bool.

Note that #32006 adds a further curtailable template whose curtailed script needs the same change, whichever merges second.

🤖 Generated with Claude Code

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In EEBus.CurtailedPercent, the percent calculation int(-limit.Value / nominal * 100) can yield values outside 0–100 or be sensitive to floating‑point rounding; consider explicitly clamping the result to [0,100] and/or rounding instead of truncating.
  • Several curtailed percent scripts (e.g. sunspec, enphase, huawei, atmoce) directly forward or derive percentages without bounds checking; it may be safer to normalize their outputs to a consistent 0–100 range to avoid unexpected values propagating through the Curtailer API.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In EEBus.CurtailedPercent, the percent calculation `int(-limit.Value / nominal * 100)` can yield values outside 0–100 or be sensitive to floating‑point rounding; consider explicitly clamping the result to [0,100] and/or rounding instead of truncating.
- Several curtailed percent scripts (e.g. sunspec, enphase, huawei, atmoce) directly forward or derive percentages without bounds checking; it may be safer to normalize their outputs to a consistent 0–100 range to avoid unexpected values propagating through the Curtailer API.

## Individual Comments

### Comment 1
<location path="meter/eebus.go" line_range="272-281" />
<code_context>
 	c.mu.Lock()
 	defer c.mu.Unlock()

 	limit, err := eebusReadValue(c.eg.EgLPPInterface, c.egLppEntity, eebus.LPPLimit, c.eg.EgLPPInterface.ProductionLimit)
 	if err != nil {
-		return false, err
+		return 0, err
+	}
+
+	// production limits are negative watts, a positive value is invalid
+	if !limit.IsActive || limit.Value > 0 {
+		return 100, nil
+	}
+
+	// without a nominal reference the limit cannot be expressed as a percent
+	nominal, err := c.eg.EgLPPInterface.ProductionNominalMax(c.egLppEntity)
+	if err != nil || nominal <= 0 {
+		return 0, api.ErrNotAvailable
 	}

-	// Check if limit is active and has a valid power value (valid is zero or negative)
-	return limit.IsActive && limit.Value <= 0, nil
+	return int(-limit.Value / nominal * 100), nil
 }

</code_context>
<issue_to_address>
**suggestion (bug_risk):** CurtailedPercent calculation lacks clamping and may generate out-of-range or sign-incorrect values.

`int(-limit.Value / nominal * 100)` relies on `limit.Value` always being negative and `nominal` positive. With float representations, noise, or overshoot, this can yield values <0 or >100. Consider clamping the result to a defined range (e.g. 0..100) and explicitly handling cases like `limit.Value == 0`, in line with the intended EEBus semantics, to avoid propagating unexpected values to callers and UI.

Suggested implementation:

```golang
	limit, err := eebusReadValue(c.eg.EgLPPInterface, c.egLppEntity, eebus.LPPLimit, c.eg.EgLPPInterface.ProductionLimit)

```

```golang
	limit, err := eebusReadValue(c.eg.EgLPPInterface, c.egLppEntity, eebus.LPPLimit, c.eg.EgLPPInterface.ProductionLimit)
	if err != nil {
		return 0, err
	}

	// production limits are negative watts, a positive value is invalid
	if !limit.IsActive || limit.Value > 0 {
		return 100, nil
	}

	// explicitly handle "no curtailment" when an active limit is exactly zero
	if limit.Value == 0 {
		return 0, nil
	}

	// without a nominal reference the limit cannot be expressed as a percent
	nominal, err := c.eg.EgLPPInterface.ProductionNominalMax(c.egLppEntity)
	if err != nil || nominal <= 0 {
		return 0, api.ErrNotAvailable
	}

	// calculate curtailed percent and clamp to [0, 100] to avoid propagating out-of-range values
	percent := -limit.Value / nominal * 100

	if percent < 0 {
		percent = 0
	} else if percent > 100 {
		percent = 100
	}

	return int(percent), nil

```

If `limit.Value` and `nominal` are not floating-point types, adjust the calculation to ensure floating-point division (e.g. cast to `float64` before computing `percent`) so rounding and clamping behave as expected. Also verify that the semantics of `CurtailedPercent` in the rest of the codebase expect `0` for "no curtailment" and `100` for "fully curtailed" or "fully available"; if the meaning is reversed, you may need to invert the clamping logic accordingly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread meter/eebus.go
@andig

andig commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

/cc @CiNcH83

@andig
andig merged commit 608aa36 into master Jul 21, 2026
9 checks passed
@andig
andig deleted the feat/curtailed-percent branch July 21, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant