Skip to content

Amperfied connect.solar: fix phase switching#29126

Open
premultiply wants to merge 8 commits intomasterfrom
device/amperfied-phaseswitch
Open

Amperfied connect.solar: fix phase switching#29126
premultiply wants to merge 8 commits intomasterfrom
device/amperfied-phaseswitch

Conversation

@premultiply
Copy link
Copy Markdown
Member

@premultiply premultiply commented Apr 15, 2026

This tries to fix phase-switching on Amperfied connect.solar wallboxes.
Writes to the current register (261) during an ongoing phase switch were being rejected by the charger. This PR synchronizes evcc's current updates with the wallbox's phase-switch state.

Changes

  • Disable wallbox-side waiting time by writing 0 to register 504 on startup (only when 1p3p phase-switching is enabled, i.e. connect.solar), so the wallbox no longer blocks writes with its own grace period. The waiting period is maintained within evcc's own timers instead.
  • Tolerate write errors during the switch: read the phase-switch duration from register 503 after the switch is triggered and, while that window is active, suppress modbus errors on writes to 261. wb.current is still tracked locally so interim setpoint changes are not lost.
  • Re-apply after the switch: a time.AfterFunc writes the latest wb.current to register 261 once the duration has passed, honoring any setpoint changes that arrived during the window. The callback queries Enabled() first and skips the re-apply if the charger was disabled in the meantime.
  • Thread safety: guard wb.current, wb.phases and wb.phaseSwitchEnd with a sync.Mutex since the AfterFunc callback runs concurrently with the loadpoint goroutine.
  • Diagnose: additional output for registers 261, 503, 504 and 5001.
  • Register constants annotated with their data types.

Background

Based on the approach proposed in the closed #20525, reduced to the timer-based path (the internal-phase-switch / power-register variant was dropped).

@premultiply premultiply self-assigned this Apr 15, 2026
@premultiply premultiply added the devices Specific device support label Apr 15, 2026
@premultiply premultiply marked this pull request as ready for review April 15, 2026 06:48
Copilot AI review requested due to automatic review settings April 15, 2026 06:48
sourcery-ai[bot]

This comment was marked as resolved.

This comment was marked as resolved.

@premultiply premultiply marked this pull request as draft April 15, 2026 07:09
@premultiply premultiply marked this pull request as ready for review April 15, 2026 07:38
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In phases1p3p, the pre-switch current reduction via wb.set(ampRegAmpsConfig, 60) does not update wb.current, so subsequent logic that relies on wb.current (e.g., Enable(true) restoring the last current) may behave inconsistently; consider updating wb.current under the mutex when forcing the 6 A minimum.
  • Access to wb.phases is now guarded by the mutex when it’s written and when the device returns 0 in getPhases, but not when the device returns a non-zero value; for consistency and to avoid subtle races, you might want to wrap all reads/writes of wb.phases with wb.mu.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `phases1p3p`, the pre-switch current reduction via `wb.set(ampRegAmpsConfig, 60)` does not update `wb.current`, so subsequent logic that relies on `wb.current` (e.g., `Enable(true)` restoring the last current) may behave inconsistently; consider updating `wb.current` under the mutex when forcing the 6 A minimum.
- Access to `wb.phases` is now guarded by the mutex when it’s written and when the device returns 0 in `getPhases`, but not when the device returns a non-zero value; for consistency and to avoid subtle races, you might want to wrap all reads/writes of `wb.phases` with `wb.mu`.

## Individual Comments

### Comment 1
<location path="charger/amperfied.go" line_range="249-251" />
<code_context>

 	curr := uint16(10 * current)

+	wb.mu.Lock()
+	inSwitch := time.Now().Before(wb.phaseSwitchEnd)
+	wb.mu.Unlock()
+
 	b := make([]byte, 2)
</code_context>
<issue_to_address>
**issue (bug_risk):** Current is updated even when the write fails during phase switching, which can desynchronize cached state from the device on unrelated errors.

In `MaxCurrentMillis`, when `inSwitch` is true, any error from `WriteMultipleRegisters` is ignored and `wb.current` is still updated. This makes sense for expected "reject during phase switch" errors, but it also hides other failures (e.g. connection issues) and can desync the cache from the device if the write didn’t actually succeed. Please at least log errors in the `inSwitch` path, or, if possible, distinguish expected phase-switch errors from other failures so that unexpected ones still propagate.
</issue_to_address>

### Comment 2
<location path="charger/amperfied.go" line_range="207-212" />
<code_context>

 	enabled := cur != 0
 	if enabled {
+		wb.mu.Lock()
 		wb.current = cur
+		wb.mu.Unlock()
 	}

</code_context>
<issue_to_address>
**suggestion (bug_risk):** The read-before-lock pattern in Enabled can lead to `wb.current` reflecting a stale value under concurrent updates.

Because `cur` is read and `enabled` computed before taking the lock, a concurrent `MaxCurrentMillis` call could change the configured current between the read and the `wb.current` assignment, causing `wb.current` to lag behind the actual configuration. If you need `wb.current` to always reflect the latest configuration, consider either taking the mutex for both the Modbus read and the assignment, or treating `wb.current` as a logical target and not updating it here. Otherwise, please confirm that this possible drift is acceptable.

```suggestion
	enabled := cur != 0
	// wb.current reflects the logical target current configured via MaxCurrentMillis;
	// do not update it here based on a potentially stale Modbus read.
```
</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 charger/amperfied.go
Comment thread charger/amperfied.go Outdated
@jwiesenm
Copy link
Copy Markdown

jwiesenm commented Apr 15, 2026

... edited:
My Bad. Error occurred because of error in yaml file according to other release.
Evcc is running. If phaseswitchimg works can. be tested tomorrow, when the sun rises.

@7BC
Copy link
Copy Markdown

7BC commented Apr 15, 2026

Currently testing automatic phase switching in PV mode. I’ll keep an eye on the logs for the next two days while the car is plugged in and let you know how it goes.

@7BC
Copy link
Copy Markdown

7BC commented Apr 16, 2026

This does not work as expected. I encountered logic errors and odd switching behavior, which I tried to cover with the attached logs. The trace log has been filtered to the load point and amperfied areas.

Bildschirmfoto 2026-04-16 um 18 26 58

evcc-20260416-181343-trace.log
evcc-20260416-180259-debug.log

@sebkau
Copy link
Copy Markdown

sebkau commented Apr 17, 2026

I'm also encountering issues. I only created a debug log, i hope you can work with that. To hopefully make it understandable I structured the description in 3 categories:

User interaction: Action triggered by me in the EVCC frontend
EVCC behaviour: How did EVCC react
Wallbox behaviour: How did the wallbox react according to the wallbox Webinterface

Test Setup:

Loadpoint name: Carport
Vehicle name: CLA200
Phases status wallbox: 1P
"Phases" setting in EVCC: 1-phasig
EVCC charge mode: PV

Manual phase switch from 1P to 3P

Step 1:
User interaction: As no sufficient PV surplus, charge mode changed to "Min+PV" to trigger charging
EVCC behaviour: Charging initiated, 1P
Wallbox behaviour: Charging started, 1P

Step 2:
User interaction: change "Phases" setting for loadpoint "Carport" from "1-phasig" to "3-phasig"
EVCC behaviour: Phase switch initiated as expected
Wallbox behaviour:

  • Phase switch initiated, 90 seconds block time started
  • phase switch to 3P
  • charging started after 90 seconds block time with 3P

EVCC behaviour after phase switch: 3P charging
--> Result: Phase switch from 1P to 3P successfull

Manual phase switch from 3P to 1P

User interaction: change "Phases" setting for loadpoint "Carport" from "3-phasig" to "1-phasig"
EVCC behaviour: Phase switch initiated as expected
Wallbox behaviour:

  • Phase switch initiated, 90 seconds block time started, phase switch to 1P
    - After 90s block time Wallbox does not start charging but AGAIN does a phase switch
  • Phase switch initiated, 90 seconds block time started, phase switch to 3P
  • charging started after 90 seconds block time with 3P

EVCC behaviour: Expects 1P charging, recognizes 3P charging and again triggers phase switch

From that point on it runs into a loop. Evcc triggers phase switch from 3P to 1P, wallbox switches phases from 3P to 1P, wallbox immediatley switches back again from 1P to 3P, wallbox starts charging. EVCC initiates phase switch again...

--> Result: Phase switch from 3P to 1P not successfull

The key would be to figure out why when switching from 3P to 1P immediatley after the first phase switch a second one is triggered.

I hope that helps, I did my best.

evcc-20260417-103059-debug.log

@premultiply
Copy link
Copy Markdown
Member Author

Manual phase switch from 3P to 1P

The whole log file never shows any 3p charging.

@sebkau
Copy link
Copy Markdown

sebkau commented Apr 17, 2026

Maybe I triggered the manual phase switch from 3P back to 1P too fast (within the EVCC update interval, 30s configured in my case). New log attached, Similar behaviour

evcc-20260417-112854-debug.log

@jwiesenm
Copy link
Copy Markdown

I can confirm same behaviour as reported by @7BC
I also reveived logic errors

Phase switching did not work. Only when triggered manually.
evcc-20260417-114411-debug.log
evcc-20260417-114116-trace.log
image001

Hope the log files are sufficient to support the development process.
Cheers !

@sebkau
Copy link
Copy Markdown

sebkau commented Apr 17, 2026

@jwiesenm
Does the manual phase switch from 3P to 1P work reliably with this version? Are you not running in the same issue I am describing above?

1P to 3P always works for me but 3P to 1P does not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

devices Specific device support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants