Skip to content

Add read-only SAJ eManager C1 Pro driver - #210

Merged
davmoz merged 2 commits into
mainfrom
cursor/saj-emanager-4909
Nov 4, 2026
Merged

davmoz merged 2 commits into
mainfrom
cursor/saj-emanager-4909

Conversation

@davmoz

@davmoz davmoz commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds saj-emanager 0.1.1, a community, read-only driver for a SAJ eManager C1 Pro sitting in front of 1..N CHS2/CH2 C&I hybrids (the intended first site is Castellum Örebro, 3x CHS2 50 kW).

It publishes aggregate PV, battery and grid telemetry over Modbus TCP. It does not write. Nothing in this change has been run on a controller.

Emitted signs match the rest of this repository (docs/WRITING-A-DRIVER.md, drivers/lua/saj.lua):

  • PV generation is negative.
  • Battery power is positive when charging and negative when discharging.
  • Grid power is positive on import and negative on export.

FTW's bundled copy does not move with this commit. Bump the pin and run scripts/sync-bundled-drivers.sh after merge.

Register sources

Decoded from Modbus_registers_eManager.xlsx (eManager + SEC-C), posted by remkoteselle on stanus74/home-assistant-saj-h2-modbus discussion #182. SAJ-formatted, not an official SAJ publication. Sheets used:

  • Interface spec — Modbus TCP on port 502. The RTU row says "Not supported". The manifest stays protocol: modbus because that is the only Modbus value the catalog allows; the description, connection default (port 502) and notes state TCP only.
  • EMS Mapping table — 0x4A00 device count, then per inverter SN (10 words) + id, stride 12, up to 21.
  • Real-time Data Table — 0x848A–0x849D system power, 0x849E–0x84B5 grid per phase, 0x8500–0x854F energy counters.
  • EMS Energy Dispatch Control — 0x8400–0x8405, documented and not read or written.
  • sec-c Anti-Backflow Control — same addresses, different meanings. Not used.

Cross-checks, not the map this driver reads:

What it emits

Holding FC 0x03. A failed or short read leaves the field out. It is not reported as 0.

  • PV from TotalPVPower (0x848F, uint32 W). The register is a magnitude. The driver negates it, so generation is negative, as in the other PV drivers. Lifetime generation from 0x8506 (×0.01 kWh → Wh).
  • Battery from TotalBatteryPower (0x8491). The H2 wire is discharge-positive, so this driver negates it: charging is positive, discharging is negative. Emitted only when capacity, SoC or battery power says a pack is there. SoC is ×0.01% → a 0..1 fraction (7325 → 0.7325). Charge/discharge totals from 0x850E / 0x8516.
  • Meter from TotalGridPowerWatt (0x8496) plus R/S/T voltage, current, frequency and power at 0x849E. The H2 grid word is already import-positive, so these watts are not negated: import is positive, export is negative. Phase current uses the same direction (positive into the site).
  • Inverter (0x848D), load (0x8493) and generator (0x8498) words have no stated sign. They are *_vendor metrics, not DER fields.
  • Identity is the first connected inverter SN. The sheet has no eManager serial. DeviceNum is 0x4A00; only that many slots are read (stride 12, max 21). The ID15 reserved decimal 19234 disagrees with hex 4AB4 (19124); the stride follows the hex.
  • 0x8550 overlaps the reserve at 0x8551 and is not read.

Assumptions

  • 32-bit values are high word first, as on the H2 map. The sheet does not say.
  • Realtime registers are holding, because the control block in the same map is R/W.
  • The wire on 0x8491 is discharge-positive (H2 0x40A6, and the 0x8405 setpoint text). One negation produces the site sign. Unverified on this aggregate register.
  • The wire on 0x8496 and the phase watt registers is import-positive (H2 SysGridPowerWall). No negation. Unverified on this aggregate register.
  • TotalPVPower is an unsigned magnitude.
  • Capacity unit "K" is not kWh. The raw word is a presence signal and a metric, not Wh.
  • FeedIn (0x852E) and Sell (0x8536) are vendor metrics, not meter import/export. SAJ uses both words both ways.
  • Voltage and frequency are divided by DeviceNum only when the sheet's own scale is implausible for one phase (above ~320 V or ~70 Hz) and the quotient lands in a normal band (~180..280 V, ~45..65 Hz). A 230 V / 50 Hz reading is left alone even with three inverters. A 690 V / 150 Hz reading with DeviceNum = 3 becomes 230 V / 50 Hz. Power, current, energy and rated power are never divided. The sheet does not document a sum; Eniris does.

Control — not implemented

0x8400 EMSEnable, 0x8401 EMSKeepTime (heartbeat, default 10 s, timeout outputs 0), 0x8402 EMSINVPRef, 0x8403/0x8404 current limits (sheet: "Temporarily Closed"), 0x8405 EMSBATPRef.

Setpoint scaling is unverified. Range on the sheet is ±11000, magnification -4, unit %. ×10^-4 makes 10000 = 1.00 and 11000 = 1.10, i.e. ±110% of rated at 0.01 percentage points per step. The device text and OpenEMS CH2 both say a positive setpoint discharges (the opposite of the site sign). This driver does not write, so that device sign is not applied to telemetry. SEC-C reuses 0x8400–0x8404 as an anti-backflow mode with different meanings.

Open questions to verify at Castellum Örebro

3x CHS2 50 kW behind one eManager C1 Pro. Compare against the site meter and the elekeeper/app figures.

  1. Does phase voltage read ~230 V or ~690 V, and frequency ~50 Hz or ~150 Hz? The divisor metric saj_emanager_phase_scale_divisor is 1 when the sheet scale was published and 3 when it was divided.
  2. Grid sign. While importing, is 0x8496 positive on the wire? This driver publishes that word unchanged, so a positive wire word is an import.
  3. Battery sign. While charging, is 0x8491 negative on the wire? This driver negates it, so a negative wire word is published as a positive charge. If the wire is already positive while charging, the negation is wrong.
  4. Word order. A known power of a few kW must match. A swapped int32 of a negative power is a huge value, not a sign flip.
  5. Function code. If FC 0x03 returns illegal function, the block may be input registers.
  6. Battery presence. With the packs online, is paralltotalcap (0x849A) non-zero, and what does unit "K" actually mean? A PV-only eManager must stay silent on the battery DER.
  7. Which of FeedIn and Sell is import?
  8. Is the first inverter SN stable enough to be the controller's identity, and is DeviceNum 3?
  9. Rated power at 0x848B: 150 kW (sum) or 50 kW (one inverter)?
  10. Setpoint scaling, only if control is attempted later. Do not write 0x8400 until held zero, charge, discharge, heartbeat refresh and a release back to 0 are shown on this firmware. Confirm it is the EMS map and not the SEC-C anti-backflow map. A future write has to translate site charge-positive into the device's discharge-positive setpoint.

Driver evidence

  • Driver id: saj-emanager
  • Device make/model: SAJ eManager C1 Pro, in front of CHS2/CH2
  • Firmware tested: none
  • Protocol/register source: discussion feat(vehicles): a car says its cable is in and where it is #182 workbook, Eniris eManager-C1 Pro notes, OpenEMS CH2 module as a cross-check
  • Hardware test performed: none. Fixture tests only (make test-driver ID=saj-emanager, make check)
  • Known limits: read-only; the wire signs and word order are unverified (the emitted signs are the catalog's); no eManager serial in the map; FeedIn/Sell not mapped to import/export; capacity not converted to Wh

Safety

  • The driver starts read-only, or this is a separately reviewed control change.
  • Vendor signs are converted at the driver boundary.
  • Cached telemetry becomes stale instead of being re-emitted as fresh.
  • No credentials, serial numbers, private addresses or site data are included.

Control evidence

Not applicable. driver_command refuses every action except init/deinit, and driver_default_mode does not write. 0x8400 is not read.

  • HIL evidence or required: not applicable (read-only)
  • Control changes include default-mode, lease-expiry and HIL evidence. — not applicable

Checks

  • Commits include Signed-off-by.
  • make test-driver ID=saj-emanager
  • make check
Open in Web Open in Cursor 

cursoragent and others added 2 commits November 4, 2026 11:30
The community register sheet does not state sign, word order, or
whether phase voltage and frequency are summed. Control stays off
until a named eManager proves a held zero.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: David Mozart <davmoz@users.noreply.github.com>
Generation stays negative. Battery charge is positive and discharge
is negative; the H2 wire is the other way round, so 0x8491 is negated.
Grid import stays positive. Vendor-only words are not DER fields.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: David Mozart <davmoz@users.noreply.github.com>
@davmoz
davmoz merged commit d1c6a2e into main Nov 4, 2026
5 checks passed
@davmoz
davmoz deleted the cursor/saj-emanager-4909 branch November 4, 2026 11:42
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