Skip to content

Conversation

@Maschga
Copy link
Collaborator

@Maschga Maschga commented Nov 8, 2025

Depends on evcc-io/rct#5
Ref #23450 (comment)

MUST BE TESTED IN NIGHTLY

2️⃣ support second battery power
🏃‍➡️ use concurrent calls whenever possible
🖱 add battery parameter

TODO:

  • rename battery to batterynumber (?)

\cc @andig

@Maschga Maschga added the devices Specific device support label Nov 8, 2025
@zeitisen
Copy link

zeitisen commented Nov 8, 2025

Wow 🤩. Wenn das im nightly drin ist, teste ich das

@premultiply
Copy link
Member

Üblicherweise adressieren und Instanzieren wir die Batterien einzeln über eine Art Index oder Umschalter, da sie ggf. unterschiedliche Eigenschaften wie z.B. soc und auch capacity haben können.

Der Gesamt-SoC wird automatisch über die site berechnet.

Wäre das hier im Sinne der Einheitlichkeit auch möglich?

@zeitisen
Copy link

zeitisen commented Nov 9, 2025

Dazu müsste die zweite Software Instanz Batterie wissen, dass sie immer die Nr. 2 auf dieser Adresse ist. Nur zu wissen, dass es zwei gibt, reicht nicht.
Ich weiß nicht, ob das die Struktur hergibt. Das müssen die wissen, die das programmiert haben.

@zeitisen
Copy link

zeitisen commented Nov 9, 2025

Ich habe jetzt gerade eine zweite Batterie angelegt.
Da hier auch Power angezeigt wird, müsste die dann auch berücksichtigt werden.

	case "battery":
		return m.queryFloat(rct.BatteryPowerW)

This is the total power of batteries as in https://rctclient.readthedocs.io/en/latest/inverter_registry.html#g-sync
0x400F015B g_sync.p_acc_lp Battery power

siehe auch weltenwort/home-assistant-rct-power-integration#396 (comment)

Hier nochmal die bekannten Werte für die zweite Batterie:

key="battery_placeholder[0].bms_sn",
key="battery_placeholder[0].bms_software_version",
key="battery_placeholder[0].module_sn[0]",
key="battery_placeholder[0].module_sn[1]",
key="battery_placeholder[0].module_sn[2]",
key="battery_placeholder[0].module_sn[3]",
key="battery_placeholder[0].module_sn[4]",
key="battery_placeholder[0].module_sn[5]",
key="battery_placeholder[0].current",
key="battery_placeholder[0].voltage",
key="battery_placeholder[0].maximum_charge_voltage",
key="battery_placeholder[0].minimum_discharge_voltage",
key="battery_placeholder[0].maximum_discharge_current",
key="battery_placeholder[0].temperature",
key="battery_placeholder[0].ah_capacity",
key="battery_placeholder[0].soc",
key="battery_placeholder[0].soh",
key="battery_placeholder[0].status2",                            

@Maschga
Copy link
Collaborator Author

Maschga commented Nov 9, 2025

Das ist der aktuelle Stand. Diese Register werden derzeit von evcc verwendet.
Ich muss wissen, welche Register für den zweiten Batterieturm gleich bleiben und welche ersetzt werden müssen.
Ich habe keinen zweiten Batterieturm und kann hier leider nichts testen und nur beschränkt helfen.

@zeitisen Kannst du diese Tabelle vervollständigen? Ich hoffe, dass hierfür die RCT Power App mit der Darstellung der Batterie-Werte helfen kann.

evcc-io/rct Registry Naming Usage Identifier for first Battery tower Replace for second battery tower (?) Identifier for second battery tower (?)
BatteryBatStatus Read 0x70A2AF4F
PowerMngSocStrategy Write 0xF168B748
BatterySoCTargetMin Write 0xCE266F0F
PowerMngBatteryPowerExternW Write 0xBD008E29
PowerMngUseGridPowerEnable Write 0x36A9E9A6
BatteryPowerW Read 0x400f015b
TotalEnergyBattInWh Read 0x5570401B
TotalEnergyBattOutWh Read 0xA9033880
BatterySoC Read 0x959930BF battery_placeholder[0].soc 0x8B4BE168

@andig
Copy link
Member

andig commented Nov 10, 2025

Entweder: wir lesen alle Batterien immer aus. Oder: wir machen die Batterie konfigurierbar. Bei WW-Speichern funktioniert das über tempsource. Für Batterien haben ich bisher nichts gefunden.

@andig
Copy link
Member

andig commented Nov 10, 2025

Gefunden:

  - name: battery
    default: 1
    type: choice
    description:
      en: Battery number
      de: Batteriespeichernummer
    choice: [1, 2]
    usages: ["battery"]

@Maschga
Copy link
Collaborator Author

Maschga commented Nov 10, 2025

Ich würde es gerne konfigurierbar machen. Auch, damit nicht unnötige Requests gestellt werden, wenn man nur einen Batterieturm hat.
Und weil es eine schönere Darstellung ist beide Batterien im evcc Dashboard angezeigt zu bekommen.

@zeitisen
Copy link

evcc-io/rct Registry Naming Usage Identifier for first Battery tower Replace for second battery tower (?) Identifier for second battery tower (?)
BatteryBatStatus Read 0x70A2AF4F  this is only for both   (1)
PowerMngSocStrategy Write 0xF168B748  only one strategy for both  --
BatterySoCTargetMin Write 0xCE266F0F  only one value for both  --
PowerMngBatteryPowerExternW Write 0xBD008E29  only one value for both  --
PowerMngUseGridPowerEnable Write 0x36A9E9A6  only one value for both  --
BatteryPowerW Read 0x400f015b this is only the sum of both   (2)
TotalEnergyBattInWh Read 0x5570401B  only one value for both  --
TotalEnergyBattOutWh Read 0xA9033880 only one value for both   --
BatterySoC Read 0x959930BF battery_placeholder[0].soc 0x8B4BE168

(1) There is a better status, which is available for both batteries (see weltenwort/home-assistant-rct-power-integration#396 (comment)) and here svalouch/python-rctclient#39
BatteryBatStatus2 | READ | 0xDE3D20D | battery_placeholder[0].status2 | 0x5C93093B,

(2) Battery power has to be calculated from current * voltage
battery.current | 0x21961B58 | battery_placeholder[0].current | 0x79D7D617 |
battery.voltage | 0x65EED11B | battery_placeholder[0].voltage | 0xFCA1CBB5 |

(3) You can get the number of batteries: power_mng.n_batteries 0x663F1452

@Maschga
Copy link
Collaborator Author

Maschga commented Nov 10, 2025

@zeitisen Danke für die hilfreichen Informationen.
Kannst du diesen PR bei dir ausprobieren?

@zeitisen
Copy link

PR ist PreRelease? Ich bin mit der Abkürzung nicht vertraut. Wenn es im nightly build drin ist, probiere ich es morgen früh aus. Sonst brauche ich eine Hilfe, wie ich das testen kann. Bei mir läuft evcc als Debianpaket auf dem Pi.

@Maschga
Copy link
Collaborator Author

Maschga commented Nov 11, 2025

PR steht für Pull Request.
Den müsstest du dir selber kompilieren und ausführen.
Oder aber der PR wird gemergt und schafft es ins Nightly, dann kannst du es mit dem Nightly ausprobieren.

@Maschga Maschga marked this pull request as ready for review November 11, 2025 07:44
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:

  • After querying numberOfBatteries, validate that the configured battery index falls within [1, numberOfBatteries] and return an error if it doesn’t to avoid silent misconfiguration.
  • The parameter and struct field named "battery" is ambiguous—consider renaming it to "batteryNumber" or "batteryIndex" now for improved clarity.
  • When using errgroup for concurrent writes/reads, consider switching to errgroup.WithContext so that a failing operation can cancel the others immediately.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- After querying numberOfBatteries, validate that the configured battery index falls within [1, numberOfBatteries] and return an error if it doesn’t to avoid silent misconfiguration.
- The parameter and struct field named "battery" is ambiguous—consider renaming it to "batteryNumber" or "batteryIndex" now for improved clarity.
- When using errgroup for concurrent writes/reads, consider switching to errgroup.WithContext so that a failing operation can cancel the others immediately.

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.

@zeitisen
Copy link

PR steht für Pull Request. Den müsstest du dir selber kompilieren und ausführen. Oder aber der PR wird gemergt und schafft es ins Nightly, dann kannst du es mit dem Nightly ausprobieren.

Ich habe gerade einen update gemacht. Ist nicht drin im nightly. Selber kompilieren wollte ich eigentlich vermeiden. Ich bin mit den Werkzeugen und dem Workflow auf Github nicht wirklich vertraut. Angefangen habe ich mal vor drei Jahren, aber nie komplett durchgezogen. Und jetzt mit einer neuen Programmiersprache zu starten, die ich vor sechs Wochen das erste Mal gesehen habe, ist mir im Moment zu viel Aufwand. Vielleicht in den nächsten Wochen mal.
Im Moment muss ich auf das Nightly warten.

@Maschga
Copy link
Collaborator Author

Maschga commented Nov 11, 2025

Kein Problem.
Der PR ist erst im Nightly, wenn er gemergt wurde.
Das dauert noch etwas.

@andig andig marked this pull request as draft November 12, 2025 17:06
@github-actions github-actions bot added the stale Outdated and ready to close label Nov 21, 2025
@Maschga Maschga marked this pull request as ready for review November 21, 2025 15:32
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:

  • The concurrent use of m.conn.Write via errgroup in batteryMode assumes rct.Connection is safe for parallel writes; if that’s not guaranteed, consider serializing these calls or adding synchronization to avoid subtle connection issues.
  • When usage == "battery", you already query PowerMngNBatteries; it would be helpful to validate the configured battery index (1/2) against this value and fail fast or clamp if the requested battery is not actually available.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The concurrent use of m.conn.Write via errgroup in batteryMode assumes rct.Connection is safe for parallel writes; if that’s not guaranteed, consider serializing these calls or adding synchronization to avoid subtle connection issues.
- When usage == "battery", you already query PowerMngNBatteries; it would be helpful to validate the configured battery index (1/2) against this value and fail fast or clamp if the requested battery is not actually available.

## Individual Comments

### Comment 1
<location> `meter/rct.go:133` </location>
<code_context>
 				}
 			}

+			var eg errgroup.Group
+
 			switch mode {
</code_context>

<issue_to_address>
**issue (complexity):** Consider reverting the new concurrency and generic helper changes in favor of simpler sequential code and explicit typed helpers while keeping the new battery features intact.

The added concurrency and generic helper do increase complexity without clear benefit. You can keep all new battery-related behavior while simplifying the implementation.

1) Simplify batteryMode: remove errgroup and use straight-line writes

The `batteryMode` closure is still fundamentally sequential configuration over a single connection. The `errgroup` adds concurrency and error aggregation overhead for no real gain.

You can keep the new `BatteryStatus2` check and all the register writes, but revert to simple sequential calls with early returns:

```go
batteryMode = func(mode api.BatteryMode) error {
	if mode != api.BatteryNormal {
		batStatus, err := m.queryInt32(rct.BatteryStatus2)
		if err != nil {
			return err
		}

		// see https://github.com/weltenwort/home-assistant-rct-power-integration/issues/264#issuecomment-2124811644
		if batStatus != 0 {
			return errors.New("invalid battery operating mode")
		}
	}

	switch mode {
	case api.BatteryNormal:
		if err := m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetInternal}); err != nil {
			return err
		}
		if err := m.conn.Write(rct.BatterySoCTargetMin, m.floatVal(float32(batterySocLimits.MinSoc)/100)); err != nil {
			return err
		}
		return m.conn.Write(rct.PowerMngBatteryPowerExternW, m.floatVal(0))

	case api.BatteryHold:
		if err := m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetInternal}); err != nil {
			return err
		}
		return m.conn.Write(rct.BatterySoCTargetMin, m.floatVal(float32(batterySocLimits.MaxSoc)/100))

	case api.BatteryCharge:
		if err := m.conn.Write(rct.PowerMngUseGridPowerEnable, []byte{1}); err != nil {
			return err
		}
		if err := m.conn.Write(rct.PowerMngBatteryPowerExternW, m.floatVal(float32(-maxchargepower))); err != nil {
			return err
		}
		return m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetExternal})

	default:
		return api.ErrNotAvailable
	}
}
```

This keeps all battery modes and limits intact while avoiding goroutines and `errgroup` for a single connection.

2) Simplify `CurrentPower` battery branch: sequential queries with a small helper

The battery branch currently has two goroutines, plus duplicated logic for selecting identifiers for battery 1 vs 2. You can:

- Pull the identifier selection into a small helper
- Do the two queries sequentially (performance impact is negligible here)

For example:

```go
func (m *RCT) batteryIVIDs() (currentID, voltageID rct.Identifier) {
	if m.battery == 2 {
		return rct.BatteryPlaceholder0Current, rct.BatteryPlaceholder0Voltage
	}
	return rct.BatteryCurrent, rct.BatteryVoltage
}

func (m *RCT) CurrentPower() (float64, error) {
	switch m.usage {
	case "grid":
		// unchanged...
	case "battery":
		currentID, voltageID := m.batteryIVIDs()

		current, err := m.queryFloat(currentID)
		if err != nil {
			return 0, err
		}
		voltage, err := m.queryFloat(voltageID)
		if err != nil {
			return 0, err
		}
		return current * voltage, nil

	default:
		return 0, fmt.Errorf("invalid usage: %s", m.usage)
	}
}
```

This preserves:

- Support for `Battery` and `BatteryPlaceholder0` IDs
- Existing behavior of computing power as `current * voltage`

without introducing goroutines or shared mutable variables.

3) Simplify retry helpers: drop generics and keep explicit typed helpers

The new `queryRCT[T any]` generic adds an abstraction layer for just three call sites. The previous pattern with a small `bo()` helper and per-type wrappers was simpler to follow.

You can keep the new `queryUint8` functionality and the `PowerMngNBatteries` query, but reintroduce the explicit helpers:

```go
func (m *RCT) bo() *backoff.ExponentialBackOff {
	return backoff.NewExponentialBackOff(
		backoff.WithInitialInterval(500*time.Millisecond),
		backoff.WithMaxInterval(2*time.Second),
		backoff.WithMaxElapsedTime(10*time.Second),
	)
}

func (m *RCT) queryFloat(id rct.Identifier) (float64, error) {
	res, err := backoff.RetryWithData(func() (float32, error) {
		return m.conn.QueryFloat32(id)
	}, m.bo())
	return float64(res), err
}

func (m *RCT) queryInt32(id rct.Identifier) (int32, error) {
	return backoff.RetryWithData(func() (int32, error) {
		return m.conn.QueryInt32(id)
	}, m.bo())
}

func (m *RCT) queryUint8(id rct.Identifier) (uint8, error) {
	return backoff.RetryWithData(func() (uint8, error) {
		return m.conn.QueryUint8(id)
	}, m.bo())
}
```

This:

- Keeps the same retry behavior and parameters
- Preserves the new `queryUint8` functionality for `PowerMngNBatteries`
- Avoids generics while keeping the code very explicit and easy to trace

All of the above changes retain your new features (battery selection, `numberOfBatteries` division, `BatteryStatus2`, `BatteryPlaceholder0*` IDs, `queryUint8`) but remove concurrency and generic abstraction where they don’t add much value.
</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.

@Maschga Maschga removed the stale Outdated and ready to close label Nov 21, 2025
@Maschga
Copy link
Collaborator Author

Maschga commented Nov 24, 2025

@andig Can you take another look at this when you have time?
Would be great to have it in nightly so that @zeitisen can test it.

@andig
Copy link
Member

andig commented Nov 24, 2025

Tbo I'm not happy with this PR. Replacing power with current x voltage and the odd energy consumption do not feel right.

@Maschga
Copy link
Collaborator Author

Maschga commented Nov 24, 2025

Since we always know how many batteries are connected to an inverter, we could use the old approach when there is only one battery and the "odd" approach when two batteries are connected.

@andig
Copy link
Member

andig commented Nov 24, 2025

If that gives cleaner implementation, I'd prefer that

@Maschga
Copy link
Collaborator Author

Maschga commented Nov 24, 2025

I'll try it out.

@Maschga
Copy link
Collaborator Author

Maschga commented Nov 24, 2025

Please have a look at the latest changes. I have tried to make the code as simple and readable as possible.

uri: {{ .host }}
usage: {{ .usage }}
cache: {{ .cache }}
battery: {{ .battery }}
Copy link
Member

Choose a reason for hiding this comment

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

remove?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Needed for batterySoc and currentPower.
Or am I missing something?

batteryMode = func(mode api.BatteryMode) error {
if mode != api.BatteryNormal {
batStatus, err := m.queryInt32(rct.BatteryBatStatus)
batStatus, err := m.queryInt32(rct.BatteryStatus2)
Copy link
Member

Choose a reason for hiding this comment

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

why?

Copy link
Collaborator Author

@Maschga Maschga Nov 24, 2025

Choose a reason for hiding this comment

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

(1) There is a better status, which is available for both batteries

See #25162 (comment)

I observed that the other one battery status (OID = 0x70A2AF4F, alias "battery.bat_status") does not show all status anymore

See svalouch/python-rctclient#39 (comment)

meter/rct.go Outdated
if m.numberOfBatteries == 1 {
return m.queryFloat(rct.BatteryPowerW)
} else {
idCurrent, idVoltage := rct.BatteryCurrent, rct.BatteryVoltage
Copy link
Member

Choose a reason for hiding this comment

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

why don't we just use the power for the first battery???

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If only one battery is connected, we use the power.
If two batteries are connected we have to calculate the power from current and voltage because in this case BatteryPowerW is the sum of both.

See #25162 (comment)

Copy link
Member

Choose a reason for hiding this comment

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

because in this case BatteryPowerW is the sum of both.

Why not simply remove the battery config and always read all batteries? Removes this issue and the weird energy thing!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I understood #25162 (comment) as a guideline to read each battery separately.

Copy link
Member

Choose a reason for hiding this comment

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

Yes. But it doesn't properly work here...

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I see. RCT is such a pain... Can you read the capacities? Then you'd have a virtual soc. Any solution remain have-baked unfortunately.

Copy link
Collaborator Author

@Maschga Maschga Nov 24, 2025

Choose a reason for hiding this comment

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

Having searched through this pdf.
There is

  • 0xB57B59BD battery.ah_capacity Battery capacity [Ah]
  • 0xBD95C46C battery_placeholder[0].ah_capacity Battery capacity [Ah]

but no registry for both batteries.

Copy link
Member

Choose a reason for hiding this comment

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

Seems like they've intentionally made it impossible my omitting parameters ;)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's quite possible. :-/

Copy link
Collaborator Author

@Maschga Maschga Nov 24, 2025

Choose a reason for hiding this comment

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

How can I proceed from here?
I would very much like to support the second battery.
Even if the solution in this PR is not the one we were looking for, it should still work and be optimized where possible.

return (in - out) / 1000, err
totalWh := (in - out) / 1000

if m.numberOfBatteries == 2 {
Copy link
Member

Choose a reason for hiding this comment

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

again, this is still odd. why don't we just read all connected batteries?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If only one battery is connected, TotalEnergyBattInWh and TotalEnergyBattOutWh are perfectly matched.
If two batteries are connected, I see no other option than to read TotalEnergyBattInWh and TotalEnergyBattOutWh and then divide by two, as it appears that both registers represent the total energy of both batteries.
In other words, I have not found any separate registers that would allow me to query the energy for each individual battery.

Copy link

@cubic-dev-ai cubic-dev-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.

No issues found across 6 files

@andig andig marked this pull request as draft November 24, 2025 15:50
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.

4 participants