Skip to content

Commit 6ddbc90

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/issue-630-subfloor-solar-export
2 parents c685187 + 64045e5 commit 6ddbc90

8 files changed

Lines changed: 297 additions & 16 deletions

File tree

.claude/skills/implement-issue/SKILL.md

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ runners — only repo-level `.claude/skills/` and `.claude/agents/` exist there.
5858

5959
| Step | CI mode |
6060
|---|---|
61-
| 0. Resume check | Applies, and matters more here: Stage 3 is re-triggered by hand, so a second `@claude-bot fix` on an issue that already has a `has-fix-pr` PR is a resume, not a restart. Detect the existing PR and continue it — never open a second PR for one issue. The CI checkout has no worktrees, so branch existence on `origin` is the only signal available. |
61+
| 0. Resume check | Applies, and matters more here: Stage 3 is re-triggered by hand, so a second `@claude-bot fix` on an issue that already has a `has-fix-pr` PR is a resume, not a restart. Detect the existing PR and continue it — never open a second PR for one issue. The CI checkout has no worktrees, so branch existence on `origin` is the only signal available. Reading the PR's **conversation comments** matters more here too, not less: the re-trigger is itself a comment, so the maintainer has very often said *why* in the same thread. |
6262
| 2. Diagnose | Stage 2 comment absent → STOP. Post "No deep analysis found. Run `@claude-bot analyze` first" and exit — never self-diagnose in CI; the analyze/fix split *is* the human gate. |
6363
| 3. Confirm gate | The owner's `@claude-bot fix` comment is the go-ahead. Still perform the workaround check and scope assessment — put them in a `## Scope assessment` section of the PR body instead of chat. Escalation path (can't confidently pass the workaround check) still applies: dispatch a fresh general-purpose `Agent` to critique the design before implementing. |
6464
| 4. Worktree | Skip — the CI checkout is already isolated. Create the branch directly (naming per Step 1). |
@@ -85,10 +85,14 @@ for `TODO.md` items and for refactors that never had an issue, so a PR with no
8585
linked issue is the normal shape for that work, not a defect.
8686

8787
```bash
88-
gh pr view <n> --json number,headRefName,isDraft,mergeable,reviews 2>/dev/null \
88+
gh pr view <n> --json number,headRefName,isDraft,mergeable,reviews,comments 2>/dev/null \
8989
|| gh issue view <n> --json number,title,labels,body,comments
9090
```
9191

92+
**`comments` is in that list deliberately — do not drop it.** It is a separate
93+
feed from `reviews`, it is where the maintainer sets direction, and omitting it
94+
has already cost one full rework (see Rehydrate, below).
95+
9296
If `<n>` is a **PR**, resume from it directly — it is the stronger handle,
9397
carrying the branch, the diff, the `## Scope assessment` and the review verdict,
9498
which is everything the table below reads. Read its linked issue too if it
@@ -127,8 +131,38 @@ this skill already forces it to be written down:
127131
discriminate. Interactive mode keeps its scope assessment conversational, so
128132
that heading is absent on most PRs this skill opens; the PR's existence is
129133
what proves Step 9 was reached, not any particular heading
134+
- **the PR CONVERSATION comments, not only its reviews** — see below
130135
- the diff itself, and any inline review comments
131136

137+
**Reviews and conversation comments are two different feeds, and `gh pr view
138+
--json reviews` returns only the first.** Read both, every time:
139+
140+
```bash
141+
gh pr view <n> --json reviews --jq '.reviews[] | "\(.author.login) \(.state) \(.submittedAt)\n\(.body)"'
142+
gh pr view <n> --json comments --jq '.comments[] | "\(.author.login) \(.createdAt)\n\(.body)"'
143+
```
144+
145+
**A maintainer conversation comment OUTRANKS every bot review on the PR**,
146+
including ones submitted after it. The bot reviews the diff; the maintainer
147+
decides the direction, and they change direction in comments — that is the
148+
only place they can, since a review has to attach to a diff.
149+
150+
This is not hypothetical. On #620 the maintainer posted "**this PR should
151+
shrink**" with a four-step plan: branch protection now rejects pushes to `main`
152+
server-side, so the protected-ref *enumeration* should be **deleted** rather
153+
than extended. A later session read the reviews, did not read the comments,
154+
and spent a full rework **adding** protected-ref patterns — the exact opposite
155+
of the standing instruction, on a PR whose own thread already said so. Two
156+
further holes the maintainer had found by hand (`git push origin
157+
refs/tags/v1.2.3`, `git push origin HEAD`) were in that comment too, and stayed
158+
open because nobody read it.
159+
160+
So: **before touching code on a resumed PR, read the human comments first, and
161+
newest-first.** If one sets a direction the diff contradicts, that is a
162+
STOP-and-confirm, not something to reconcile silently — the maintainer may have
163+
changed their mind since, and asking costs one message where guessing costs a
164+
rework.
165+
132166
**If those sources do not reconstruct a coherent diagnosis, STOP and report
133167
it.** Do not re-diagnose from scratch on top of someone else's half-finished
134168
branch: you would be building on a design you cannot see, and the commits
@@ -674,6 +708,20 @@ On the verdict:
674708
silently ignore it either.
675709
- **A review from the maintainer rather than the bot** (the `<author>` field):
676710
treat it as authoritative and stop the loop — a human has taken over.
711+
- **A maintainer CONVERSATION comment, which is not a review at all**, is
712+
equally authoritative and arrives on a feed the verdict never touches. Check
713+
it each round, not only at Step 0 — a direction posted mid-loop is invisible
714+
to `gh pr view --json reviews`, and to the bot, which will keep reviewing the
715+
diff as if nothing had been said:
716+
717+
```bash
718+
gh pr view <n> --json comments \
719+
--jq '.comments[] | select(.createdAt > "<submittedAt from the round before>") | "\(.author.login)\n\(.body)"'
720+
```
721+
722+
If one lands, stop the loop and act on it before the next round. A bot
723+
APPROVED on a diff the maintainer has already asked you to redo is worth
724+
nothing.
677725

678726
Fix the blockers, park genuine nits in `TODO.md`, run
679727
`./scripts/quality-check.sh`, commit, and push. Step 9's frontend rule carries
@@ -756,6 +804,8 @@ net is upstream, not this section.
756804
| "the old branch is a mess, cleaner to redo it" | Its commits are the only copy of a diagnosis you no longer have. If you genuinely cannot reconstruct the approach, that is a STOP-and-report, not a licence to reset. |
757805
| "that worktree's session shows dead, so it's mine to take" | Check unsandboxed. A sandboxed `claude agents --json` returned 1 session where the real answer was 17, because `~/.claude/jobs` is sandbox-denied — every other session read as dead. |
758806
| "the review said CHANGES_REQUESTED but nobody assigned it to me" | Nothing else will pick it up. Once the opening session exits, an orphaned PR has no owner at all — `sweep-prs` refuses the job by design. Resuming is how it gets one. |
807+
| "I read the reviews, so I know what this PR needs" | Reviews and conversation comments are separate feeds and `--json reviews` returns only one. The maintainer sets *direction* in comments, because a review can only attach to a diff. On #620 that cost a full rework in the opposite direction. |
808+
| "the bot approved it, so the direction must be fine" | The bot reviews the diff against a checklist; it has no idea what the maintainer asked for in the thread. An approval on a diff you were told to redo is worth nothing. |
759809
| "I can see the assertion is right, no need to run it red" | Assertions that look right have repeatedly bounded only one side, or compared a quantity a second varying term swamped. Seeing it fail is the cheap part. |
760810
| "quality-check.sh passed, that's enough" | Green tests prove the suite is satisfied, not that the fix behaves correctly against the real scenario. Step 8 requires observed output, every time. |
761811
| "the diagnosis is obviously right, skip the confirm gate" | Wrong diagnoses are exactly when confidence is highest. One message, cheap insurance. |
@@ -787,6 +837,12 @@ net is upstream, not this section.
787837
- About to re-diagnose from scratch on top of someone else's half-finished
788838
branch because the Stage 2 comment and PR body didn't reconstruct the
789839
approach. That is a STOP-and-report.
840+
- **About to change code on a resumed PR without having read its conversation
841+
comments** — not just its reviews. They are separate feeds, and the
842+
maintainer's direction lives in the one `--json reviews` does not return.
843+
- About to implement a diff that contradicts a maintainer comment already on
844+
the thread. Stop and confirm; they may have moved on, but guessing costs a
845+
rework and asking costs one message.
790846
- About to open a second PR for an issue that already has one.
791847
- About to relaunch an issue that has already died twice without saying so.
792848
- About to commit or open the PR without having actually run/observed the

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
2323
### Fixed
2424

2525
- **A tiny solar surplus is no longer planned as an export the inverter will absorb** — below the export the plan can express, the battery charged anyway and ran fuller than planned, spilling the difference later. ([#630](https://github.com/johanzander/bess-manager/issues/630))
26+
- **The setup wizard no longer locks you out of an inverter platform it failed to auto-detect** — every platform stays selectable, and a re-scan keeps the one you picked. ([#621](https://github.com/johanzander/bess-manager/issues/621))
2627
- **System no longer gets stuck on "initializing" when many consecutive periods are near-tied** — a long run of volatile prices could make every hourly optimization fail, leaving no schedule at all. ([#624](https://github.com/johanzander/bess-manager/issues/624))
2728
- **Grid charging now reaches the planned amount instead of stopping just short** — the charge rate is written as a whole percent, and rounding it down meant the battery charged slightly less than the plan counted on.
2829
- **Growatt VPP no longer briefly executes the previous period's power command when switching modes** — enabling remote control commits immediately, so the power target is now written before it, and cleared on release. ([#593](https://github.com/johanzander/bess-manager/issues/593))

backend/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2962,6 +2962,7 @@ async def run_setup_discovery():
29622962
{
29632963
"growatt_found": integrations["growatt_found"],
29642964
"growatt_device_id": integrations["growatt_device_id"],
2965+
"huawei_found": integrations["huawei_found"],
29652966
"huawei_device_id": integrations.get("huawei_device_id"),
29662967
"solax_found": integrations["solax_found"],
29672968
"solax_has_growatt_tou": integrations.get(

backend/tests/test_setup_api.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,7 @@ def test_octopus_only_persists_gbp_defaults(self):
953953
integrations = {
954954
"growatt_found": False,
955955
"growatt_device_id": None,
956+
"huawei_found": False,
956957
"solax_found": False,
957958
"nordpool_found": False,
958959
"nordpool_area": None,
@@ -986,6 +987,7 @@ def test_nordpool_discovery_does_not_clear_costs(self):
986987
integrations = {
987988
"growatt_found": False,
988989
"growatt_device_id": None,
990+
"huawei_found": False,
989991
"solax_found": False,
990992
"nordpool_found": True,
991993
"nordpool_area": "SE3",
@@ -1019,6 +1021,7 @@ def test_norwegian_nordpool_updates_currency_preserves_costs(self):
10191021
integrations = {
10201022
"growatt_found": False,
10211023
"growatt_device_id": None,
1024+
"huawei_found": False,
10221025
"solax_found": False,
10231026
"nordpool_found": True,
10241027
"nordpool_area": "NO1",
@@ -1049,6 +1052,7 @@ def test_no_locale_hints_leaves_defaults_unchanged(self):
10491052
integrations = {
10501053
"growatt_found": False,
10511054
"growatt_device_id": None,
1055+
"huawei_found": False,
10521056
"solax_found": False,
10531057
"nordpool_found": False,
10541058
"nordpool_area": None,
@@ -1080,6 +1084,7 @@ def test_discover_optional_sensors_receives_entity_registry(self):
10801084
integrations = {
10811085
"growatt_found": False,
10821086
"growatt_device_id": None,
1087+
"huawei_found": False,
10831088
"solax_found": False,
10841089
"nordpool_found": False,
10851090
"nordpool_area": None,
@@ -1108,6 +1113,96 @@ def test_discover_optional_sensors_receives_entity_registry(self):
11081113
)
11091114

11101115

1116+
class TestDiscoverForwardsInverterDetectionFlags:
1117+
"""POST /api/setup/discover must forward a detection flag for EVERY
1118+
inverter platform the wizard shows (#621).
1119+
1120+
`discover_integrations()` produces `huawei_found`, but the endpoint
1121+
dropped it while forwarding the other three. The wizard's
1122+
`DiscoveryResult` declares `huaweiFound: boolean` (non-optional), so the
1123+
missing key surfaced as `undefined` rather than a type error, and the
1124+
Huawei detection dot read grey for every user including a correctly
1125+
detected stock `huawei_solar` install.
1126+
1127+
Asserting the flag on `discover_integrations()` alone is what let this
1128+
through — `test_scenario_discovery.py::...` already did that and passed.
1129+
The gap is in the endpoint's payload, so that is what these assert.
1130+
"""
1131+
1132+
def _run_discover(self, ctrl, integrations):
1133+
ha = ctrl.ha_controller
1134+
ha.discover_integrations.return_value = (integrations, [])
1135+
ha.fetch_entity_registry.return_value = []
1136+
ha.discover_sensors_from_registry.return_value = ({}, None, {})
1137+
ha.discover_current_sensors.return_value = {}
1138+
ha.discover_optional_sensors.return_value = {}
1139+
ha.discover_octopus_entities.return_value = {}
1140+
ha.ENTITY_SUFFIX_MAP = {}
1141+
ha.SOLAX_GROWATT_MIN_SUFFIX_MAP = {}
1142+
ha.SOLAX_GROWATT_SPH_SUFFIX_MAP = {}
1143+
ha.SOLAX_NATIVE_SUFFIX_MAP = {}
1144+
sys.modules["app"].bess_controller = ctrl
1145+
return _client.post("/api/setup/discover")
1146+
1147+
@staticmethod
1148+
def _integrations(**overrides):
1149+
base = {
1150+
"growatt_found": False,
1151+
"growatt_device_id": None,
1152+
"huawei_found": False,
1153+
"huawei_device_id": None,
1154+
"solax_found": False,
1155+
"solis_found": False,
1156+
"nordpool_found": False,
1157+
"nordpool_area": None,
1158+
"nordpool_custom_area": None,
1159+
"nordpool_custom_entity": None,
1160+
"nordpool_config_entry_id": None,
1161+
"octopus_found": False,
1162+
"detected_inverter_platforms": [],
1163+
"detected_phase_count": None,
1164+
"currency": None,
1165+
"vat_multiplier": None,
1166+
}
1167+
base.update(overrides)
1168+
return base
1169+
1170+
def test_every_platform_detection_flag_is_present_in_the_payload(self):
1171+
"""All four wizard platform tabs need their flag, not just three."""
1172+
ctrl = _make_discover_controller(deepcopy(_PRE_EXISTING_STORE))
1173+
resp = self._run_discover(ctrl, self._integrations())
1174+
1175+
assert resp.status_code == 200
1176+
body = resp.json()
1177+
for key in ("growattFound", "solaxFound", "solisFound", "huaweiFound"):
1178+
assert key in body, f"{key} missing from /api/setup/discover payload"
1179+
1180+
def test_detected_huawei_is_reported_as_found(self):
1181+
"""A stock huawei_solar install must light the Huawei dot green."""
1182+
ctrl = _make_discover_controller(deepcopy(_PRE_EXISTING_STORE))
1183+
resp = self._run_discover(
1184+
ctrl,
1185+
self._integrations(
1186+
huawei_found=True,
1187+
huawei_device_id="dev-huawei-1",
1188+
detected_inverter_platforms=["huawei_solar_luna2000"],
1189+
),
1190+
)
1191+
1192+
assert resp.status_code == 200
1193+
assert resp.json()["huaweiFound"] is True
1194+
1195+
def test_undetected_huawei_is_reported_as_not_found(self):
1196+
"""The reporter's case: EMMA integration, so the flag is False --
1197+
False, not absent. The wizard must still be able to offer the tab.
1198+
"""
1199+
ctrl = _make_discover_controller(deepcopy(_PRE_EXISTING_STORE))
1200+
resp = self._run_discover(ctrl, self._integrations(huawei_found=False))
1201+
1202+
assert resp.status_code == 200
1203+
assert resp.json()["huaweiFound"] is False
1204+
1205+
11111206
class TestDiscoverReportsDisabledSensors:
11121207
"""POST /api/setup/discover must tell the wizard which sensors are
11131208
unmapped because their entity is disabled in HA (#549).
@@ -1123,6 +1218,7 @@ def _run_discover(self, ctrl, platform_sensors, platform_disabled, platform):
11231218
{
11241219
"growatt_found": False,
11251220
"growatt_device_id": None,
1221+
"huawei_found": False,
11261222
"solax_found": True,
11271223
"nordpool_found": False,
11281224
"nordpool_area": None,
@@ -1243,6 +1339,7 @@ def _integrations(self, **overrides) -> dict:
12431339
base = {
12441340
"growatt_found": False,
12451341
"growatt_device_id": None,
1342+
"huawei_found": False,
12461343
"solax_found": False,
12471344
"nordpool_found": False,
12481345
"nordpool_area": None,

docs/SOFTWARE_DESIGN.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,12 +494,21 @@ On first startup with no sensors configured, or when the user triggers discovery
494494

495495
The HA WebSocket API (`config/entity_registry/list`) returns every registered entity with its `platform` field.
496496

497+
Matching is by exact platform name, so a supported inverter reached through a
498+
different integration is not detected — Huawei LUNA2000/EMMA via
499+
`huawei_emma_management` rather than the stock `huawei_solar`, for example.
500+
Detection therefore narrows the wizard's **defaults**, never its **choices**:
501+
every platform stays selectable so such a user can pick theirs and map the
502+
sensors by hand (#621).
503+
497504
Detected integrations:
498505

499506
| Category | HA Platform | Detected As |
500507
|-----------|---------------------|-------------|
501508
| Inverter | `growatt_server` | Growatt |
502509
| Inverter | `solax_modbus` | SolaX |
510+
| Inverter | `solis_modbus` | Solis |
511+
| Inverter | `huawei_solar` | Huawei |
503512
| Price | `nordpool` | Nordpool |
504513
| Price | `octopus_energy` | Octopus Energy |
505514
| Forecast | `solcast_solar` | Solcast solar forecast |
@@ -597,7 +606,7 @@ The setup wizard is a 6-step flow for first-time configuration. It is triggered
597606
#### Wizard Steps (Frontend: `SetupWizardPage.tsx`)
598607

599608
1. **Scan** — Calls `/api/setup/discover` to auto-detect integrations and sensors
600-
2. **Review Sensors** — Displays discovered sensor mappings, allows manual correction, selects inverter platform. Blocked while a required sensor is unmapped, or while a required sensor's only entity is disabled in HA (the entities to enable are listed by name)
609+
2. **Review Sensors** — Displays discovered sensor mappings, allows manual correction, selects inverter platform. Every platform is selectable regardless of what was detected; the per-platform status dot reports detection, and the auto-detected platform is merely preselected. Blocked while a required sensor is unmapped, or while a required sensor's only entity is disabled in HA (the entities to enable are listed by name)
601610
3. **Electricity Pricing** — Configure price area, provider (Nordpool/Octopus), markup, VAT (pre-filled from discovery hints). Blocked until the selected provider's required configuration is filled in, mirroring the server-side check on `/api/setup/complete`
602611
4. **Battery** — Set capacity, SOC limits, power rating, cycle cost
603612
5. **Home** — Set consumption, fuse current, voltage, phase count (pre-filled from detected phase count)

0 commit comments

Comments
 (0)