Skip to content

fix: harden SI doors, HA discovery slugs, and charging identities - #970

Merged
cursor[bot] merged 3 commits into
masterfrom
cursor/harden-units-charge-math-89f4
Aug 22, 2026
Merged

fix: harden SI doors, HA discovery slugs, and charging identities#970
cursor[bot] merged 3 commits into
masterfrom
cursor/harden-units-charge-math-89f4

Conversation

@frahlg

@frahlg frahlg commented Aug 22, 2026

Copy link
Copy Markdown
Member

Accepted text proposal

Issue or Discussion: maintainer request to harden charging/units math against edge cases after v2.2.1-beta.1. Also covers #960 (HA MQTT illegal discovery topics).

Maintainer comment that accepted this scope: start a broad hardening pass on tests, edge cases, units, charging logic, and strange math errors.

What changed

  • Units doors (go/internal/units): PercentFromFraction / permille fold NaN/Inf/overflow onto [0,1]/[0,100]/[0,1000]. CanonicalPowerEnergy is case-insensitive and zeros non-finite values. FractionFromLegacyPercent treats (1, 2) as 0–1 overflow (BMS 1.02), not as 1.02%. Values ≥2 still divide by 100; 150 stays 1.5 so ValidFraction can reject it.
  • HA bridge (#960): discovery object ids and driver state topics slug YAML names that are not already [A-Za-z0-9_-]+. Illegal names keep a readable stem and append an FNV-1a tag of the original string (Laddare, GarageLaddare_Garage_57d42421) so comma vs space do not collide. Already-legal names, including mixed case (Ev-Charger_1), are returned unchanged so Home Assistant does not keep a retained entity next to a new lowercase copy. Friendly name is unchanged. SoC publishes through PercentFromFraction.
  • App proto: battery SoC on the wire uses PermilleFromFraction (NaN → 0 permille). Boost lease args clamp after the legacy-percent door.
  • Dev backfill: synthetic bat_soc is a 0–1 fraction (was 0–100, so the dashboard showed ~5000% after -backfill).
  • Charging identity (go/internal/sitepower): tested site-boundary math:
  • Extra tests: telemetry ValidateReading (percent SoC, NaN, +Inf, 1.02), energy ledger later export does not shrink import, battery-boost JSON round-trip.

Why

Post-SI-core, several doors still treated overflow as percent (1.021.02%), published NaN, stored backfill SoC as 0–100, and built HA discovery topics from raw YAML names. Surplus-only EV vs leftover PV is easy to get wrong if you look at the meter sign.

Codex P2s on mqttObjectID (commit 454e8a1b): lowercase slugging changed already-legal mixed-case unique_ids, and comma vs space collapsed onto one slug. 121570ca keeps legal ids stable and tags illegal names with FNV-1a of the original string.

Boundaries and safety

Out of scope on purpose — open PRs own those files:

  • #968 owns main.go, config.go, planner prefs / UI
  • #957 owns mpc.go, loadpoint controller / surplus_reserve.go
  • #888 owns dispatch.go

sitepower is the identity those drafts should adopt. Production DP still uses evW > 0 && gridW > 50. Idle-hybrid deadband (#822), driver-YAML max_charge_w: 0 → 5 kW (main.go), and the <1000 kWp-vs-watts heuristic are documented, not “fixed under” those PRs.

Stale telemetry, planner output, and driver default-mode paths are unchanged. No UI files.

Verification

  • gofmt on every edited Go file
  • go test ./internal/units ./internal/sitepower ./internal/devtools ./internal/ha ./internal/appproto ./internal/loadpoint ./internal/telemetry ./internal/state ./internal/api — pass
  • go test ./internal/control ./internal/mpc ./internal/calendar ./internal/config — pass
  • go test ./internal/ha after 121570ca — pass (legal mixed-case ids unchanged; comma vs space distinct)
  • First CI pass on 5a3c5a48: core, full stack, go test + vet green; no new legacy product copy failed because the HA fixture copied forty_two_watts. Fixture now uses site_box.
  • Driver Lua tests were not run here: this environment has no drivers/*.lua snapshot (pre-existing, not this diff)

Checklist

  • The diff implements one accepted scope and does not add follow-on work.
  • I checked open pull requests that touch the same files.
  • Tests cover the changed behaviour and its failure path.
  • A human reviewed every changed web/UI view in a browser, or no UI changed.
  • A Changeset is included, or the change is exempt.
  • Every commit has a DCO sign-off.
Open in Web Open in Cursor 

cursoragent and others added 2 commits August 22, 2026 17:52
Stop NaN/overflow SoC from leaking as percent or Inf at the units, app, and
HA doors. Slug MQTT discovery object ids so driver names with spaces or
commas are not silently dropped. Store synthetic history SoC as 0–1.
Lock leftover-PV vs battery-grid-buy math in a tested sitepower identity
without rewriting files owned by open planner/dispatch drafts.

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
The brand-cleanup gate treats new "forty_two_watts" copy as unclassified.
The illegal-topic repro is the driver name, not the box id.

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@frahlg
frahlg marked this pull request as ready for review August 22, 2026 17:59

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 454e8a1b45

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/internal/ha/bridge.go Outdated
Comment on lines +379 to +380
for _, r := range strings.ToLower(name) {
ok := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make slugged driver identifiers collision-resistant

When two valid configured driver names normalize identically, such as Garage and garage or Laddare, Garage and Laddare Garage, this lossy slug assigns both drivers the same discovery IDs and state topics. Their Home Assistant entities overwrite one another and subsequent state publications become indistinguishable; include a stable collision-resistant component instead of using the slug as the identity.

Useful? React with 👍 / 👎.

Comment thread go/internal/ha/bridge.go
Comment on lines +398 to +400
func (b *Bridge) driverUniqueID(driver, suffix string) string {
return b.deviceID + "_" + mqttObjectID(driver) + suffix
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep existing legal discovery IDs stable

For an existing driver whose name is already legal but contains uppercase characters, such as the test's Ev-Charger_1, this changes its unique_id and discovery topic on upgrade even though the declared allowed set includes uppercase. Because discovery configs are retained and the old topic is never removed, Home Assistant keeps the old entity while registering a new lowercase one, leaving users with duplicate and stale entities; preserve the original identifiers for already-legal names or explicitly migrate/remove the retained topics.

Useful? React with 👍 / 👎.

Leave already-legal MQTT object ids, including mixed case, unchanged so
Home Assistant does not keep a retained Ev-Charger_1 entity next to a
new lowercase copy. Illegal names keep a readable stem and append an
FNV-1a tag of the original string so "Laddare, Garage" and "Laddare
Garage" do not share a topic.

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Left a non-blocking comment: Cursor Bugbot was still running after 8 minutes, so this PR is not approved. Reviewers were assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@cursor
cursor Bot requested a review from erikarenhill August 22, 2026 18:13

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approved. Cursor Bugbot completed successfully and reported no findings that need human review. No reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@cursor
cursor Bot merged commit 31742d7 into master Aug 22, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants