Skip to content

Feat/kaco nh3 hybrid template - #33044

Draft
hansenelectric wants to merge 11 commits into
evcc-io:masterfrom
hansenelectric:feat/kaco-nh3-hybrid-template
Draft

Feat/kaco nh3 hybrid template#33044
hansenelectric wants to merge 11 commits into
evcc-io:masterfrom
hansenelectric:feat/kaco-nh3-hybrid-template

Conversation

@hansenelectric

@hansenelectric hansenelectric commented Aug 21, 2026

Copy link
Copy Markdown

Add KACO NH3 Hybrid Inverter Template (Modbus-RTU)

Description

Add complete support for KACO Blueplanet Hybrid NH3 inverters with full hybrid functionality and automatic inverter synchronization.

Features

  • Protocol: Modbus-RTU (RS485) with configurable baud rate (9600, 19200, 38400)
  • Modes: Grid, PV (with curtailment), and Battery
  • SunSpec Compliance: Standard SunSpec models (701, 702, 703, 704, 714, 802)
  • Dynamic Power Calculation: Charge/discharge limits automatically calculated from real-time battery voltage (P = U × 30A)
  • Auto-Sync Battery Settings: Capacity, min/max SOC read directly from inverter (Model 802)
  • Curtailment Support: Via SunSpec Model 704 (Generic Inverter Controls)
  • Battery Control: Full battery mode control (normal/hold/charge)

Verified Against

  • KACO Blueplanet Hybrid NH3 SunSpec PICS template
  • Product models: 103, 120, 121, 123, 160, 201, 702, 704, 714, 802
  • Device: bp 12.0 NH3 M2
  • Firmware: SunSpec compliant (V2.1.7)
  • Interface: Modbus-RTU (9600 baud, Slave ID 3)

Key Improvements

  • ✅ Modbus-RTU support (more common than TCP in residential systems)
  • ✅ Full hybrid support (grid + PV + battery)
  • ✅ Dynamic voltage-based power limits (adapts to multi-series battery packs like Dyness 6-series: 300-330V)
  • ✅ Zero manual configuration - battery parameters auto-read from inverter
  • ✅ Auto-sync with inverter settings (change SOC limits on inverter, EVCC reads them automatically)
  • ✅ Standard SunSpec compliance (vendor-agnostic)
  • ✅ Accurate AC power readings (Model 701/703)
  • ✅ Grid power monitoring via 3-phase meter model (Model 203)

Notes

  • Tested with 6-series lithium battery stacks (voltage range: 300-330V)
  • All battery parameters (capacity, minsoc, maxsoc) dynamically read from inverter via Model 802
  • Max charge/discharge current: 30A (inverter limitation)

…id, Battery)\n\n- Modbus-RTU (RS485) connection support with configurable baud rate\n- Grid, PV (with curtailment), and Battery usage modes\n- SunSpec compliant (Models 701, 702, 703, 704, 714, 802)\n- Dynamic power calculation from battery voltage (P = U × 30A)\n- Full battery control (normal/hold/charge modes)\n- Curtailment via Model 704 with Model 123 fallback\n\nVerified against KACO Blueplanet Hybrid NH3 SunSpec PICS template
@github-actions github-actions Bot added devices Specific device support enhancement New feature or request labels Aug 21, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The requirements.description text hardcodes a 9600 baud Modbus-RTU connection, but the template exposes a configurable baudrate param (9600/19200/38400); consider updating the description so it aligns with the actual configuration options.
  • The modBusDeviceId parameter name is inconsistent with the other parameter naming (mixed casing vs. lowercase with no camelCase); consider normalizing this to match existing conventions for easier template reuse and discoverability.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `requirements.description` text hardcodes a 9600 baud Modbus-RTU connection, but the template exposes a configurable `baudrate` param (9600/19200/38400); consider updating the description so it aligns with the actual configuration options.
- The `modBusDeviceId` parameter name is inconsistent with the other parameter naming (mixed casing vs. lowercase with no camelCase); consider normalizing this to match existing conventions for easier template reuse and discoverability.

## Individual Comments

### Comment 1
<location path="templates/definition/meter/kaco-nh3-hybrid.yaml" line_range="118" />
<code_context>
+  power:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value: {{ if eq .battery "2" }}714:Prt.2.DCW{{ else }}714:Prt.1.DCW{{ end }} # Model 714 (DC Module - Battery Power)
+    # Prt.1 = Battery 1, Prt.2 = Battery 2 via Model 714 (Multi-Port DC Module)
+  soc:
</code_context>
<issue_to_address>
**issue (bug_risk):** Align the battery parameter type with the comparison to avoid a non-matching condition.

`battery` is defined as a numeric choice (`[1, 2]`), so YAML will parse it as integers, but the template compares it to a string (`"2"`). That condition will never be true, and `Prt.1.DCW` will always be used. Please either make the choices strings (`["1", "2"]`) or compare against an integer (`if eq .battery 2`) so the correct port is selected.
</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 templates/definition/meter/kaco-nh3-hybrid.yaml Outdated
- Fix hardcoded baud rate in requirements.description to mention configurable options (9600, 19200, 38400)
- Normalize parameter naming: modBusDeviceId → modbusdeviceid (lowercase, no camelCase)
- Fix battery parameter type mismatch: change choice from [1, 2] to ["1", "2"] for string comparison
- Battery selector now correctly uses string comparison (if eq .battery "2") matching the parameter type
…SunSpec Model 802

- Add capacity source: 802:WHRtg (Battery nominal capacity in Wh)
- Add minsoc source: 802:SoCMin (Minimum SOC limit % from inverter)
- Add maxsoc source: 802:SoCMax (Maximum SOC limit % from inverter)
- Remove manual parameter definitions - all values now read from inverter
- Parameters automatically sync with inverter settings (no manual configuration needed)
- If SoC limits change on inverter, EVCC reads new values automatically
config:
source: sunspec
{{- include "modbus" . | indent 6 }}
value: 704:WMaxLimPctEna # Model 704 at address 50639

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

drop the comments, they don't provide additional value

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

okey. i put them in so i could remember where it was coming from. but okey. i can leave them out.

Comment thread templates/definition/meter/kaco-nh3-hybrid.yaml Outdated
# Formula: Power (W) = Battery Voltage (V) × Max Current (30 A)
# Battery voltage varies with SoC (e.g., 300-330V for 6-series Dyness stack)
# This ensures accurate power limits throughout the charge cycle
maxchargepower:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

pls keep it simple- too much magic

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

hmm. i would like this limitation as the charging/discharging power is highly dependent on the battery voltage (for high voltage batteries). if it was a low voltage battery then it's more simpel.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We're not doing this for any other (HV) battery sofar. Lets keep it consistent and simple.

@andig
andig marked this pull request as draft August 21, 2026 11:19
Co-authored-by: andig <cpuidle@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Found 2 high-confidence issues in the new template.

  1. Unused modbusdeviceid param — wrong Modbus slave ID applied
    File: templates/definition/meter/kaco-nh3-hybrid.yaml, lines 14-18 (
    - name: modbusdeviceid
    default: 3
    description:
    de: Geräte-ID (Slave ID)
    en: Device ID (Slave ID)
    )

The custom modbusdeviceid param (default 3) is never referenced in the render section below. Since the template does not define a param literally named id, evcc auto-injects the standard id param (see ModbusParams in

// check if the modbus params are already added
if index, _ := t.ParamByName("id"); index >= 0 {
return
}
, which only skips injection when a param named id already exists), defaulting to 1 per
definitions:
- name: id
description:
generic: Modbus ID
default: 1
type: int
. It is id, not modbusdeviceid, that actually drives the rendered id field via modbus.tpl. As written, users will see a Geraete-ID (Slave ID) field defaulting to 3 that has no effect, while the real Modbus slave id silently defaults to 1, contradicting the PR description's stated default of Slave ID 3, so communication with the device will fail unless the user separately discovers and sets the id field. Suggested fix: rename this param to id.

  1. Invalid SunSpec point selector Prt.N.DCW
    File: templates/definition/meter/kaco-nh3-hybrid.yaml, line 119 (
    source: sunspec
    {{- include "modbus" . | indent 2 }}
    value: {{ if eq .battery "2" }}714:Prt.2.DCW{{ else }}714:Prt.1.DCW{{ end }} # Model 714 (DC Module - Battery Power)
    # Prt.1 = Battery 1, Prt.2 = Battery 2 via Model 714 (Multi-Port DC Module)
    soc:
    )

evcc's SunSpec point grammar only supports model:point or model:block:point, colon-delimited (see ParsePoint at

func ParsePoint(selector string) (SunSpecOperation, error) {
var (
res SunSpecOperation
err error
)
el := strings.Split(selector, ":")
if len(el) < 2 || len(el) > 3 {
return res, fmt.Errorf("invalid sunspec format: %s", selector)
}
if res.Model, err = strconv.Atoi(el[0]); err != nil {
return res, fmt.Errorf("invalid sunspec model: %s", selector)
}
if len(el) == 3 {
// block is the middle element
res.Block, err = strconv.Atoi(el[1])
if err != nil {
return res, fmt.Errorf("invalid sunspec block: %s", selector)
}
}
res.Point = el[len(el)-1]
return res, nil
}
). Repeating SunSpec blocks elsewhere in this repo use a numeric block index, e.g. 160:1:DCW / 160:2:DCW in
add:
- source: sunspec
{{- include "modbus" . | indent 6 }}
value: 160:1:DCW # mppt 1
- source: sunspec
{{- include "modbus" . | indent 6 }}
value: 160:2:DCW # mppt 2
energy:
. The dot-notation 714:Prt.1.DCW / 714:Prt.2.DCW used here does not match that grammar, ParsePoint will treat Prt.1.DCW as a literal point name since there is no colon to split on for a block index, so it will not resolve against the model's actual point definitions and will fail with a sunspec model not found error at startup. This likely needs block-indexed addressing instead, e.g. 714:1:DCW / 714:2:DCW.

@hansenelectric
hansenelectric marked this pull request as ready for review August 21, 2026 12:59

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In the curtail.set switch for the 100% case, you have a nested set block under source: const (const → set → sunspec), which is a different structure than the default branch and may not match the expected schema for a switch case; consider aligning it with the default branch pattern (set directly setting the Sunspec value) to avoid runtime misconfiguration.
  • The maxchargepower and maxdischargepower go scripts duplicate the same logic for reading 802:V and computing int(voltage * 30); if the template system allows, consider extracting this into a shared helper or a single computed field to avoid divergence if the formula needs updating later.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the `curtail.set` switch for the 100% case, you have a nested `set` block under `source: const` (const → set → sunspec), which is a different structure than the default branch and may not match the expected schema for a switch case; consider aligning it with the default branch pattern (`set` directly setting the Sunspec value) to avoid runtime misconfiguration.
- The `maxchargepower` and `maxdischargepower` `go` scripts duplicate the same logic for reading 802:V and computing `int(voltage * 30)`; if the template system allows, consider extracting this into a shared helper or a single computed field to avoid divergence if the formula needs updating later.

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 templates/definition/meter/kaco-nh3-hybrid.yaml Outdated
Comment thread templates/definition/meter/kaco-nh3-hybrid.yaml
@andig
andig marked this pull request as draft August 24, 2026 09:45
…ve comments

- Simplify curtailment switch default case to align with case 100 pattern
- Remove inline comments that don't add significant value
- Keep core comments for critical logic
- Improves code consistency and reduces verbosity
- All fixes address Sourcery AI review feedback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

devices Specific device support enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants