Skip to content

Add Pytes JS3 hybrid inverter (Modbus TCP) - #3994

Open
DerWahreKlinki wants to merge 3 commits into
OpenEMS:developfrom
DerWahreKlinki:pytes-upstream
Open

DerWahreKlinki wants to merge 3 commits into
OpenEMS:developfrom
DerWahreKlinki:pytes-upstream

Conversation

@DerWahreKlinki

Copy link
Copy Markdown
Collaborator

Driver for the Pytes JS3 hybrid inverter (Solis-based RS485 register map, Modbus TCP): ESS with the inverter's work state machine and remote dispatch control (battery power set-point with bias/loss feed-forward and integral trim, or AC output control), battery values from the inverter's battery port and the BMS, PV strings as DC charger, the inverter's grid meter and backup port as meters.

Features: EMS-side and inverter-side battery current limits (minimum rule), grid feed-in limitation from Core.Meta as inverter-side hardware backstop plus dynamic AC output cap (reg 43052) with a tolerance scaled to the limit, ripple control receiver support, failsafe timeout, a surplus search that keeps the PV from being locked at consumption level in AC output control, backup-port loads in AC output control, warnings that do not stop the EMS control. The readme documents both set-point modes and their measured behaviour.

Verified on a live installation (5.1 kWh Pytes battery, 15 kVA inverter) from 2026-06 to 2026-09.

Driver for the Pytes JS3 hybrid inverter (Solis-based RS485 register map,
Modbus TCP): ESS with the inverter's work state machine and remote dispatch
control (battery power set-point with bias/loss feed-forward and integral
trim, or AC output control), battery values from the inverter's battery port
and the BMS, PV strings as DC charger, the inverter's grid meter and backup
port as meters.

Features: EMS-side and inverter-side battery current limits (minimum rule),
grid feed-in limitation from Core.Meta as inverter-side hardware backstop plus
dynamic AC output cap (reg 43052) with a tolerance scaled to the limit, ripple
control receiver support, failsafe timeout, a surplus search that keeps the PV
from being locked at consumption level in AC output control, backup-port loads
in AC output control, warnings that do not stop the EMS control. The readme
documents both set-point modes and their measured behaviour.

Verified on a live installation (5.1 kWh Pytes battery, 15 kVA inverter)
from 2026-06 to 2026-09.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

❗ There is a different number of reports uploaded between BASE (a4d068b) and HEAD (f5de864). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (a4d068b) HEAD (f5de864)
java 1 0
Additional details and impacted files
@@              Coverage Diff               @@
##             develop    #3994       +/-   ##
==============================================
- Coverage      55.47%   25.93%   -29.53%     
==============================================
  Files           3861      540     -3321     
  Lines         178080    16291   -161789     
  Branches       14818     2853    -11965     
==============================================
- Hits           98771     4224    -94547     
+ Misses         75424    11879    -63545     
+ Partials        3885      188     -3697     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sfeilmeier

Copy link
Copy Markdown
Contributor

Question before we dive into the code: typically one would either configure

  • Battery-Inverter and Battery Components and combine them with a Generic-Ess; or
  • directly implement ESS

A mixture of both - like here - is usually not feasible or probably over-engineered. Why did you choose that path?

@DerWahreKlinki

Copy link
Copy Markdown
Collaborator Author

Hi Stefan,

Why a direct ESS and not BatteryInverter + Battery + Generic ESS: the Pytes JS3
(Solis-based) is a DC-coupled hybrid whose control interface is a hybrid-level "remote
dispatch" block (regs 44100-44108):
the set-point is either a battery power or an AC output power, and the inverter itself decides about PV curtailment, the backup port and its export cap.
That does not map onto the ManagedSymmetricBatteryInverter contract (an AC set-point for an AC-coupled inverter, allowed power derived by the generic ESS from battery limits x voltage).
We measured that the inverter curtails PV or ignores the set-point depending on the mode (documented in readme.adoc), and handling that needs the ESS itself - the same reason GoodWe ships a direct GoodWeEss next to its battery-inverter variant.

Why still a separate Pytes.Battery: there is no separate BMS bus - all battery data (SoC, SoH, voltage, current, BMS current limits, fault word) comes through the inverter's registers 33133-33150. We exposed them as a Battery component so that the
standard Battery nature channels are available to the UI widgets, apps and Timedata in the same way as for other systems, and to keep the ESS channel list readable. The component has no control function; it reads one register block and registers itself at the ESS, which takes BMS limits, DC power and the backup-port load from it.

We are open to change this if you prefer the plain pattern: folding Pytes.Battery into the ESS (battery values become ESS channels, no Battery nature) is a rather small change. Going the generic route instead would mean a BatteryInverter that has to synthesize a Battery from the same registers and a generic ESS that cannot express the mode-dependent inverter behaviour - we don't think that is the better fit here. Let us know which direction you'd like.

Regards,
klinki

@sfeilmeier

Copy link
Copy Markdown
Contributor

Why a direct ESS and not BatteryInverter + Battery + Generic ESS: the Pytes JS3 (Solis-based) is a DC-coupled hybrid whose control interface is a hybrid-level "remote dispatch" block (regs 44100-44108): the set-point is either a battery power or an AC output power, and the inverter itself decides about PV curtailment, the backup port and its export cap. That does not map onto the ManagedSymmetricBatteryInverter contract (an AC set-point for an AC-coupled inverter, allowed power derived by the generic ESS from battery limits x voltage).

That is very similar to how the GoodWe - and most likely every other hybrid battery inverter - behaves. I have rough plans to replace/extend the ManagedSymmetricEss.applyPower() method with something more generic in future, that allows setting DC-side set-points...

We measured that the inverter curtails PV or ignores the set-point depending on the mode (documented in readme.adoc), and handling that needs the ESS itself - the same reason GoodWe ships a direct GoodWeEss next to its battery-inverter variant.

The reason we have a separate GoodWeEss is, that there are two ways to configure a energy storage system with a GoodWe (or FENECON) battery inverter:

  1. GenericEss (Virtual) + GoodWe-Battery-Inverter (via Modbus) + FENECON Home Battery (via Modbus) -> this configuration is used, when OpenEMS communicates with each device separately, e.g. with a FENECON Home, Commercial or Industrial storage system. There is no direct communication between the battery inverter and the battery in this setup, so OpenEMS tells the inverter the current limits.
  2. GoodWeEss (via Modbus) -> this configuration is used, when a GoodWe inverter is used together with a BYD Battery-Box. There is no direct communication with the Battery from OpenEMS.

Why still a separate Pytes.Battery: there is no separate BMS bus - all battery data (SoC, SoH, voltage, current, BMS current limits, fault word) comes through the inverter's registers 33133-33150. We exposed them as a Battery component so that the standard Battery nature channels are available to the UI widgets, apps and Timedata in the same way as for other systems, and to keep the ESS channel list readable. The component has no control function; it reads one register block and registers itself at the ESS, which takes BMS limits, DC power and the backup-port load from it.
We are open to change this if you prefer the plain pattern: folding Pytes.Battery into the ESS (battery values become ESS channels, no Battery nature) is a rather small change.

So this should be variant (2) and from my opinion we should remove the Battery implementation.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants