Skip to content

Conversation

@D3c1mu2
Copy link

@D3c1mu2 D3c1mu2 commented Nov 30, 2025

Summary

  • Add E3DC Multi Connect II wallbox charger support via RSCP protocol
  • Implement all standard charger interfaces (Status, Enable, MaxCurrent)
  • Add phase switching support (1p/3p) via WB_REQ_SET_NUMBER_PHASES
  • Include meter interfaces for power, energy, and current readings
  • Add RFID identification via session data

Implemented Interfaces

Interface Method Description
api.Charger Status() A/B/C via WB_EXTERN_DATA_ALG flags
api.Charger Enable()/Enabled() Via WB_REQ_SET_ABORT_CHARGING
api.Charger MaxCurrent() Via WB_REQ_SET_MAX_CHARGE_CURRENT
api.Meter CurrentPower() Sum of WB_PM_POWER_L1/L2/L3
api.MeterEnergy TotalEnergy() DB_TEC_WALLBOX_ENERGYALL + WB_ENERGY_ALL
api.PhaseCurrents Currents() Calculated from power/230V
api.PhaseGetter GetPhases() WB_NUMBER_PHASES
api.PhaseSwitcher Phases1p3p() WB_REQ_SET_NUMBER_PHASES
api.CurrentGetter GetMaxCurrent() WB_MAX_CHARGE_CURRENT
api.CurrentLimiter GetMinMaxCurrent() WB_LOWER/UPPER_CURRENT_LIMIT
api.ChargeRater ChargedEnergy() WB_SESSION_CHARGED_ENERGY
api.ChargeTimer ChargeDuration() WB_SESSION_ACTIVE_CHARGE_TIME
api.Identifier Identify() RFID from WB_SESSION_AUTH_DATA

Test plan

  • Tested with E3DC Multi Connect II wallbox, FW 1.0.3.0
  • Verified Status A/B/C detection
  • Verified Enable/Disable charging
  • Verified current setting (6-32A)
  • Verified phase switching (1p/3p)
  • Verified power readings (~11kW at 3p/16A)
  • Verified RFID identification
  • Verified all interfaces via evcc CLI dry-run tests

Open items / Help requested

  • Testing with other E3DC wallboxes: Only tested with Multi Connect II - verification with other E3DC wallbox models appreciated
  • Code review: Please review the Go code for correctness and evcc coding conventions

Notes

  • Requires go-rscp v0.2.2+ for WB_REQ_SET_NUMBER_PHASES tag and Bool datatype support
  • Phase switching requires "Automatic phase switching" to be disabled in E3DC dashboard
  • Voltage readings not available (WB_REQ_DIAG_PHASE_VOLTAGE returns ERR_ACCESS_DENIED)
  • Session-based values (ChargedEnergy, ChargeDuration, Identify) return 0/empty when no vehicle is connected

andig and others added 9 commits March 11, 2025 14:34
Implement core charger functionality with corrected RSCP protocol handling:
- Fix Enabled() to use WB_EXTERN_DATA_ALG byte 2 bit 6
- Fix Enable() to use WB_REQ_SET_ABORT_CHARGING instead of WB_SET_EXTERN
- Fix Status() to use WB_EXTERN_DATA_ALG with correct bit flags
- Fix maxCurrent() to use WB_REQ_SET_MAX_CHARGE_CURRENT (6-32A)

Add meter and phase interfaces:
- api.Meter (CurrentPower)
- api.MeterEnergy (TotalEnergy from DB_TEC + WB_ENERGY)
- api.PhasePowers (Powers per phase)
- api.PhaseCurrents (calculated from power/230V)
- api.PhaseGetter (active phases bitmask)
- api.CurrentLimiter (6-32A range)
- api.CurrentGetter (cached current value)

Status: Testing in progress, debug output still enabled.

Note: Requires go-rscp library with Bool DataType support for
WB_REQ_SET_ABORT_CHARGING. Upstream library may need updates.
Implement all charger interfaces for E3DC Multi Connect wallbox via RSCP:

Core interfaces:
- Status() using WB_EXTERN_DATA_ALG byte flags
- Enable()/Enabled() via WB_REQ_SET_ABORT_CHARGING
- MaxCurrent() via WB_REQ_SET_MAX_CHARGE_CURRENT (6-32A)

Meter interfaces:
- CurrentPower() from WB_PM_POWER_L1/L2/L3
- TotalEnergy() combining DB_TEC_WALLBOX_ENERGYALL + WB_ENERGY_ALL
- Powers() per phase power readings
- Currents() calculated from power/230V (voltage not accessible)

Phase control:
- GetPhases() via WB_NUMBER_PHASES (not WB_PM_ACTIVE_PHASES)
- Phases1p3p() via WB_REQ_SET_NUMBER_PHASES (wallbox handles safe switching)
- GetMinMaxCurrent() via WB_LOWER/UPPER_CURRENT_LIMIT
- GetMaxCurrent() via WB_MAX_CHARGE_CURRENT

Session data:
- ChargedEnergy() from WB_PM_ENERGY_L1/L2/L3
- ChargeDuration() from WB_SESSION_ACTIVE_CHARGE_TIME
- Identify() returns RFID tag from WB_SESSION_AUTH_DATA

Tested with E3DC Multi Connect II wallbox, firmware 1.0.3.0.

Note: Requires go-rscp with WB_REQ_SET_NUMBER_PHASES and Bool datatype support.
- ChargedEnergy() now uses WB_SESSION_CHARGED_ENERGY for accurate
  per-session energy tracking (returns 0 when no session active)
- Replace license block with development status comments
- All charger interfaces tested and verified via evcc CLI
sourcery-ai[bot]

This comment was marked as outdated.

@premultiply premultiply changed the base branch from feat/e3dc-wb to master November 30, 2025 13:46
@premultiply premultiply added the devices Specific device support label Nov 30, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for the E3DC Multi Connect II wallbox charger using the RSCP protocol. The implementation provides comprehensive charger functionality including phase switching (1p/3p), RFID identification, power/energy metering, and current control.

Key changes:

  • New E3DC RSCP charger implementation with support for all standard charger interfaces plus phase switching and RFID
  • Template definition file for E3DC RSCP charger configuration
  • Helper functions for RSCP protocol message parsing and error handling

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 12 comments.

File Description
charger/e3dc.go Complete E3DC charger implementation using RSCP protocol with support for status monitoring, enable/disable control, current limiting, phase switching, power/energy metering, and RFID identification
templates/definition/charger/e3dc-rscp.yaml Configuration template defining parameters (host, port, credentials, wallbox index) and capabilities (1p3p phase switching, RFID)

charger/e3dc.go Outdated
// The sum of both values matches the total energy shown in the E3DC portal.
// Testing showed: DB_TEC (8319 kWh) + WB_ENERGY (699 kWh) = 9018 kWh ≈ Portal (9019 kWh)
func (wb *E3dc) TotalEnergy() (float64, error) {
// Query both energy sources in parallel
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The comment says "Query both energy sources in parallel", but the code executes the queries sequentially, not in parallel. The comment is misleading and should be updated to reflect the actual behavior:

// Query both energy sources sequentially
Suggested change
// Query both energy sources in parallel
// Query both energy sources sequentially

Copilot uses AI. Check for mistakes.
charger/e3dc.go Outdated
return nil, err
}

port, _ := strconv.Atoi(port_)
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The error from strconv.Atoi(port_) is silently ignored. If the port cannot be parsed, it defaults to 0, which may cause unexpected connection failures. Consider handling the error explicitly:

port, err := strconv.Atoi(port_)
if err != nil {
    return nil, fmt.Errorf("invalid port: %w", err)
}
Suggested change
port, _ := strconv.Atoi(port_)
port, err := strconv.Atoi(port_)
if err != nil {
return nil, fmt.Errorf("invalid port: %w", err)
}

Copilot uses AI. Check for mistakes.
charger/e3dc.go Outdated
}

if l := len(res); l < length {
return nil, fmt.Errorf("invalid length: expected %d, got %d", length, l)
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The length validation uses < (less than) but the error message says "expected %d". This is misleading because the function accepts containers with length >= the specified value. Consider either:

  1. Using != if exact length is required, or
  2. Updating the error message to clarify it's a minimum requirement (e.g., "expected at least %d, got %d")
Suggested change
return nil, fmt.Errorf("invalid length: expected %d, got %d", length, l)
return nil, fmt.Errorf("invalid length: expected at least %d, got %d", length, l)

Copilot uses AI. Check for mistakes.

return p1, p2, p3, nil
}

Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The power reading logic is duplicated between CurrentPower() and Powers(). Both methods fetch the same data using identical requests. Consider having CurrentPower() call Powers() and sum the results:

func (wb *E3dc) CurrentPower() (float64, error) {
    p1, p2, p3, err := wb.Powers()
    if err != nil {
        return 0, err
    }
    return p1 + p2 + p3, nil
}

This eliminates code duplication and ensures consistency between the two methods.

Suggested change
// CurrentPower returns the sum of all phase powers.
func (wb *E3dc) CurrentPower() (float64, error) {
p1, p2, p3, err := wb.Powers()
if err != nil {
return 0, err
}
return p1 + p2 + p3, nil
}

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

Powers is not needed and can be removed

charger/e3dc.go Outdated
}

// NewE3dcFromConfig creates an E3DC charger from generic config
func NewE3dcFromConfig(other map[string]interface{}) (api.Charger, error) {
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The function signature is missing context.Context as the first parameter. All charger factory functions in the codebase follow the pattern NewXFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error). The context parameter is required for proper lifecycle management and cancellation support.

Change to:

func NewE3dcFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error)

Copilot uses AI. Check for mistakes.
charger/e3dc.go Outdated
Comment on lines 53 to 58
host, port_, err := net.SplitHostPort(util.DefaultPort(cc.Uri, 5033))
if err != nil {
return nil, err
}

port, _ := strconv.Atoi(port_)
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

Variable name port_ uses a trailing underscore which is unconventional in Go. Consider using a more descriptive name like portStr to indicate it's the string representation of the port.

Suggested change
host, port_, err := net.SplitHostPort(util.DefaultPort(cc.Uri, 5033))
if err != nil {
return nil, err
}
port, _ := strconv.Atoi(port_)
host, portStr, err := net.SplitHostPort(util.DefaultPort(cc.Uri, 5033))
if err != nil {
return nil, err
}
port, _ := strconv.Atoi(portStr)

Copilot uses AI. Check for mistakes.
charger/e3dc.go Outdated
Comment on lines 436 to 517
func (wb *E3dc) ChargedEnergy() (float64, error) {
res, err := wb.conn.Send(*rscp.NewMessage(rscp.WB_REQ_SESSION, nil))
if err != nil {
return 0, err
}

sessionData, err := rscpContainer(*res, 1)
if err != nil {
return 0, err
}

// Find WB_SESSION_CHARGED_ENERGY in session data
for _, msg := range sessionData {
if msg.Tag == rscp.WB_SESSION_CHARGED_ENERGY {
energy, err := rscpFloat64(msg)
if err != nil {
return 0, err
}
return energy / 1000.0, nil // Wh -> kWh
}
}

// No active session
return 0, nil
}

var _ api.ChargeTimer = (*E3dc)(nil)

// ChargeDuration implements the api.ChargeTimer interface
// Returns the duration of the current charging session
func (wb *E3dc) ChargeDuration() (time.Duration, error) {
res, err := wb.conn.Send(*rscp.NewMessage(rscp.WB_REQ_SESSION, nil))
if err != nil {
return 0, err
}

sessionData, err := rscpContainer(*res, 1)
if err != nil {
return 0, err
}

// Find WB_SESSION_ACTIVE_CHARGE_TIME in session data
for _, msg := range sessionData {
if msg.Tag == rscp.WB_SESSION_ACTIVE_CHARGE_TIME {
// Session time is in milliseconds (Uint64)
ms, err := rscpFloat64(msg)
if err != nil {
return 0, err
}
return time.Duration(ms) * time.Millisecond, nil
}
}

// No active session
return 0, nil
}

var _ api.Identifier = (*E3dc)(nil)

// Identify implements the api.Identifier interface
// Returns the RFID tag ID used to authorize the current session
func (wb *E3dc) Identify() (string, error) {
res, err := wb.conn.Send(*rscp.NewMessage(rscp.WB_REQ_SESSION, nil))
if err != nil {
return "", err
}

sessionData, err := rscpContainer(*res, 1)
if err != nil {
return "", err
}

// Find WB_SESSION_AUTH_DATA in session data
for _, msg := range sessionData {
if msg.Tag == rscp.WB_SESSION_AUTH_DATA {
return rscpString(msg)
}
}

// No active session or no RFID used
return "", nil
}
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The session data fetching logic is duplicated across three methods (ChargedEnergy, ChargeDuration, and Identify). Consider extracting this into a helper method to reduce code duplication:

func (wb *E3dc) getSessionData() ([]rscp.Message, error) {
    res, err := wb.conn.Send(*rscp.NewMessage(rscp.WB_REQ_SESSION, nil))
    if err != nil {
        return nil, err
    }
    return rscpContainer(*res, 1)
}

This would improve maintainability and reduce the risk of inconsistencies.

Copilot uses AI. Check for mistakes.
charger/e3dc.go Outdated
Comment on lines 100 to 173
func (wb *E3dc) Enabled() (bool, error) {
res, err := wb.conn.Send(*rscp.NewMessage(rscp.WB_REQ_DATA, []rscp.Message{
*rscp.NewMessage(rscp.WB_INDEX, wb.id),
*rscp.NewMessage(rscp.WB_REQ_EXTERN_DATA_ALG, nil),
}))
if err != nil {
return false, err
}

wbData, err := rscpContainer(*res, 2)
if err != nil {
return false, err
}

wbExtDataAlg, err := rscpContainer(wbData[1], 2)
if err != nil {
return false, err
}

b, err := rscpBytes(wbExtDataAlg[1])
if err != nil {
return false, err
}

// WB_EXTERN_DATA_ALG Byte 2, Bit 6: 0 = enabled, 1 = disabled (abort active)
return b[2]&(1<<6) == 0, nil
}

// Enable implements the api.Charger interface
func (wb *E3dc) Enable(enable bool) error {
// WB_REQ_SET_ABORT_CHARGING: true = abort/stop, false = allow/resume
_, err := wb.conn.Send(*rscp.NewMessage(rscp.WB_REQ_DATA, []rscp.Message{
*rscp.NewMessage(rscp.WB_INDEX, wb.id),
*rscp.NewMessage(rscp.WB_REQ_SET_ABORT_CHARGING, !enable),
}))

return err
}

// Status implements the api.Charger interface
func (wb *E3dc) Status() (api.ChargeStatus, error) {
res, err := wb.conn.Send(*rscp.NewMessage(rscp.WB_REQ_DATA, []rscp.Message{
*rscp.NewMessage(rscp.WB_INDEX, wb.id),
*rscp.NewMessage(rscp.WB_REQ_EXTERN_DATA_ALG, nil),
}))
if err != nil {
return api.StatusNone, err
}

wbData, err := rscpContainer(*res, 2)
if err != nil {
return api.StatusNone, err
}

wbExtDataAlg, err := rscpContainer(wbData[1], 2)
if err != nil {
return api.StatusNone, err
}

b, err := rscpBytes(wbExtDataAlg[1])
if err != nil {
return api.StatusNone, err
}

// WB_EXTERN_DATA_ALG Byte 2: Bit 5 (0x20) = charging, Bit 3 (0x08) = connected
switch {
case b[2]&0x20 != 0:
return api.StatusC, nil
case b[2]&0x08 != 0:
return api.StatusB, nil
default:
return api.StatusA, nil
}
}
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The Enabled() and Status() methods fetch the same WB_EXTERN_DATA_ALG data. Consider extracting this into a shared helper method to reduce duplication:

func (wb *E3dc) getExternDataAlg() ([]byte, error) {
    res, err := wb.conn.Send(*rscp.NewMessage(rscp.WB_REQ_DATA, []rscp.Message{
        *rscp.NewMessage(rscp.WB_INDEX, wb.id),
        *rscp.NewMessage(rscp.WB_REQ_EXTERN_DATA_ALG, nil),
    }))
    if err != nil {
        return nil, err
    }
    
    wbData, err := rscpContainer(*res, 2)
    if err != nil {
        return nil, err
    }
    
    wbExtDataAlg, err := rscpContainer(wbData[1], 2)
    if err != nil {
        return nil, err
    }
    
    return rscpBytes(wbExtDataAlg[1])
}

Then both methods can simply call this helper and decode the specific bits they need.

Copilot uses AI. Check for mistakes.
@andig
Copy link
Member

andig commented Dec 1, 2025

Great PR! Please consider the curated review comments. Generally replace wbData with b which is the usual name for a slice of bytes.

Changes based on PR evcc-io#25703 review:
- Add context.Context parameter to constructor (Go conventions)
- Improve port parsing error handling
- Fix code duplication: powers() is now private, CurrentPower() uses it
- Add getExternDataAlg() and getSessionData() helpers to reduce duplication
- Add checkConfiguration() to verify wallbox settings at startup:
  - Sun mode: automatically disabled (conflicts with evcc control)
  - Auto phase switching: warning only (user may want to keep it)
- Add header documentation with configuration requirements
- Rename port_ to portStr for clarity

Phase switching (1p/3p) tested successfully with real hardware.
The wallbox handles ramping internally - evcc only needs to send the command.
@D3c1mu2
Copy link
Author

D3c1mu2 commented Dec 6, 2025

Phase Switching Test Results

Tested phase switching (1p ↔ 3p) with real E3DC Multi Connect II
Wallbox and vehicle charging.

Key findings:

  • Phase switching works correctly via WB_REQ_SET_NUMBER_PHASES
  • The wallbox handles ramping internally (reduces current →
    switches → ramps up)
  • evcc only needs to send the phase command, no additional safety
    logic required
phase_switch_plot

Test details:

  • 3p → 1p: Power ramps down from ~8.5 kW, switches, then
    stabilizes at ~2.2 kW on L1
  • 1p → 3p: Power ramps down, switches, then ramps back up on all
    phases
  • Total test duration: ~3 minutes

Also added configuration check at startup:

  • Sun mode: automatically disabled (conflicts with evcc control)
  • Auto phase switching: warning if enabled

I hope i did the right Changes for the Comments above.

@andig andig marked this pull request as draft December 6, 2025 15:03
@D3c1mu2 D3c1mu2 marked this pull request as ready for review December 6, 2025 19:21
Copy link
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 there - I've reviewed your changes - here's some feedback:

  • In ChargeDuration, the comment says the session time is a Uint64 in milliseconds but the code parses it via rscpFloat64; consider using a dedicated rscpUint64 helper to avoid precision issues and better match the protocol type.
  • In TotalEnergy, if no DB_TEC_WALLBOX_VALUE entry matches the configured wallbox id, dbEnergy remains 0 and this silently degrades the reported total energy; it may be safer to detect this case and return an error or at least log a warning about a possible misconfigured index.
  • The constructor NewE3dcFromConfig ignores the passed context.Context after creation; if the RSCP client supports it, consider threading the context through to requests or client lifetime to allow for cancellation and proper shutdown.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ChargeDuration`, the comment says the session time is a Uint64 in milliseconds but the code parses it via `rscpFloat64`; consider using a dedicated `rscpUint64` helper to avoid precision issues and better match the protocol type.
- In `TotalEnergy`, if no `DB_TEC_WALLBOX_VALUE` entry matches the configured wallbox `id`, `dbEnergy` remains 0 and this silently degrades the reported total energy; it may be safer to detect this case and return an error or at least log a warning about a possible misconfigured index.
- The constructor `NewE3dcFromConfig` ignores the passed `context.Context` after creation; if the RSCP client supports it, consider threading the context through to requests or client lifetime to allow for cancellation and proper shutdown.

## Individual Comments

### Comment 1
<location> `charger/e3dc.go:117` </location>
<code_context>
+
+// checkConfiguration verifies wallbox settings and adjusts them for evcc control
+func (wb *E3dc) checkConfiguration() {
+	res, err := wb.conn.Send(*rscp.NewMessage(rscp.WB_REQ_DATA, []rscp.Message{
+		*rscp.NewMessage(rscp.WB_INDEX, wb.id),
+		*rscp.NewMessage(rscp.WB_REQ_SUN_MODE_ACTIVE, nil),
</code_context>

<issue_to_address>
**issue (complexity):** Consider introducing small helper functions to centralize repeated RSCP request/parse patterns and session lookups to make the charger implementation easier to read and maintain.

You can reduce a lot of repetition and magic indexing with a couple of small helpers without changing behavior.

### 1. Wrap the `WB_REQ_DATA` pattern

You repeat:

```go
res, err := wb.conn.Send(*rscp.NewMessage(rscp.WB_REQ_DATA, []rscp.Message{
	*rscp.NewMessage(rscp.WB_INDEX, wb.id),
	*rscp.NewMessage(rscp.WB_REQ_XYZ, nil),
	// ...
}))
if err != nil {
	return ...
}
wbData, err := rscpContainer(*res, N)
if err != nil {
	return ...
}
```

Extract that into a helper that hides the container and length logic:

```go
func (wb *E3dc) wbData(minLen int, tags ...rscp.Tag) ([]rscp.Message, error) {
	req := make([]rscp.Message, 0, len(tags)+1)
	req = append(req, *rscp.NewMessage(rscp.WB_INDEX, wb.id))
	for _, t := range tags {
		req = append(req, *rscp.NewMessage(t, nil))
	}

	res, err := wb.conn.Send(*rscp.NewMessage(rscp.WB_REQ_DATA, req))
	if err != nil {
		return nil, err
	}

	return rscpContainer(*res, minLen)
}
```

Then e.g. `GetPhases` becomes:

```go
func (wb *E3dc) GetPhases() (int, error) {
	wbData, err := wb.wbData(2, rscp.WB_REQ_NUMBER_PHASES)
	if err != nil {
		return 0, err
	}

	phases, err := rscpUint8(wbData[1])
	if err != nil {
		return 0, err
	}

	return int(phases), nil
}
```

Similarly for `getExternDataAlg`, `powers`, `GetMinMaxCurrent`, `GetMaxCurrent`, `Phases1p3p`, parts of `Diagnose`, etc.

### 2. Add a dedicated `rscpBool` helper

You currently inline lambdas to extract bools:

```go
if sunMode, err := rscpValue(wbData[1], func(data any) (bool, error) {
	if val, ok := data.(bool); ok {
		return val, nil
	}
	return false, errors.New("invalid type")
}); err == nil && sunMode {
	// ...
}
```

This can be replaced by a simple helper:

```go
func rscpBool(msg rscp.Message) (bool, error) {
	return rscpValue(msg, func(data any) (bool, error) {
		b, ok := data.(bool)
		if !ok {
			return false, errors.New("invalid response")
		}
		return b, nil
	})
}
```

Then your call sites simplify to:

```go
sunMode, err := rscpBool(wbData[1])
if err == nil && sunMode {
	// ...
}
```

(and similarly for `autoPhaseSwitch` in `Phases1p3p` and the booleans in `Diagnose`).

### 3. Factor out session tag lookup

`ChargedEnergy`, `ChargeDuration`, and `Identify` all do the same “loop over `sessionData` and match `msg.Tag`” pattern. Extract:

```go
func (wb *E3dc) sessionMessage(tag rscp.Tag) (rscp.Message, bool, error) {
	sessionData, err := wb.getSessionData()
	if err != nil {
		return rscp.Message{}, false, err
	}

	for _, msg := range sessionData {
		if msg.Tag == tag {
			return msg, true, nil
		}
	}
	return rscp.Message{}, false, nil
}
```

Then:

```go
func (wb *E3dc) ChargedEnergy() (float64, error) {
	msg, ok, err := wb.sessionMessage(rscp.WB_SESSION_CHARGED_ENERGY)
	if err != nil || !ok {
		return 0, err
	}

	energy, err := rscpFloat64(msg)
	if err != nil {
		return 0, err
	}
	return energy / 1000.0, nil
}
```

```go
func (wb *E3dc) ChargeDuration() (time.Duration, error) {
	msg, ok, err := wb.sessionMessage(rscp.WB_SESSION_ACTIVE_CHARGE_TIME)
	if err != nil || !ok {
		return 0, err
	}

	ms, err := rscpFloat64(msg) // or a dedicated rscpUint64 if you prefer
	if err != nil {
		return 0, err
	}
	return time.Duration(ms) * time.Millisecond, nil
}
```

```go
func (wb *E3dc) Identify() (string, error) {
	msg, ok, err := wb.sessionMessage(rscp.WB_SESSION_AUTH_DATA)
	if err != nil || !ok {
		return "", err
	}
	return rscpString(msg)
}
```

These small abstractions keep all behavior the same but localize the protocol details and indexing, which should address most of the “structural and cognitive complexity” concerns.
</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.

@D3c1mu2 D3c1mu2 marked this pull request as draft December 6, 2025 19:37
- Add rscpBool helper for boolean RSCP values
- Add rscpUint64 helper for correct millisecond parsing
- Add sessionMessage helper to reduce code duplication
- Add warning when wallbox index not found in DB
- Improve code documentation throughout
@D3c1mu2 D3c1mu2 marked this pull request as ready for review December 6, 2025 20:22
Copy link
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 there - I've reviewed your changes - here's some feedback:

  • In NewE3dc, the global rscp.Log is unconditionally set to DebugLevel for the entire process; consider aligning the RSCP log level with evcc's configured logger level (or enabling debug only when the charger logger is in trace/debug) to avoid excessive logging in production.
  • The auto phase switching check in Phases1p3p duplicates part of the configuration validation already done in checkConfiguration; consider centralizing this check or caching the flag so you don't need to re-query RSCP on every phase switch while still failing fast if the configuration is incompatible.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `NewE3dc`, the global `rscp.Log` is unconditionally set to `DebugLevel` for the entire process; consider aligning the RSCP log level with evcc's configured logger level (or enabling debug only when the charger logger is in trace/debug) to avoid excessive logging in production.
- The auto phase switching check in `Phases1p3p` duplicates part of the configuration validation already done in `checkConfiguration`; consider centralizing this check or caching the flag so you don't need to re-query RSCP on every phase switch while still failing fast if the configuration is incompatible.

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.

D3c1mu2 and others added 2 commits December 6, 2025 21:58
Address code review feedback:
- Add ensureSunModeDisabled() check before Enable(), MaxCurrent(), and
  Phases1p3p() since users can re-enable sun mode in the E3DC portal
  at any time without restarting evcc
- Add comments explaining why RSCP log level and auto phase switch
  checks are implemented the way they are (not bugs, intentional design)
- Extract disableSunMode() helper for reuse
@D3c1mu2 D3c1mu2 marked this pull request as draft December 8, 2025 15:17
@andig
Copy link
Member

andig commented Dec 8, 2025

@D3c1mu2 can I kindly ask you to contact [email protected]?

@D3c1mu2
Copy link
Author

D3c1mu2 commented Dec 8, 2025

@D3c1mu2 can I kindly ask you to contact [email protected]?

Email shoud be sended.

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.

3 participants