DER Control Register Reference¶
Complete reference of all DER control registers on the FranklinWH aGate, documenting what we use, what works, what's untested, and the future test plan.
Cross-references: ORCHESTRATION_AND_CONTROL.md (legacy command sequencing), FRANKLINWH_SUNSPEC_QUIRKS.md (known quirks)
1. Model 704 — DER Control AC (Battery Control)¶
Active Power Control (WSet Group)¶
These are the primary battery control registers. This is what we use.
| Address | Field | Label | Type | RW | Used | Status | Code Reference |
|---|---|---|---|---|---|---|---|
| 40318 | WSetEna |
Active Power Enable | enum16 | RW | ✅ | ✅ Working | send_command() step 1 (disable=0) & 3 (enable=1) |
| 40319 | WSetMod |
Active Power Mode | enum16 | RW | ✅ | ✅ Working | send_command() step 2 (set=0) |
| 40320 | WSet |
Active Power Setpoint (W) | int32 | RW | ❌ | ⚠️ Avoided | Not used — causes mode flickering when set alongside WSetPct |
| 40322 | WSetRvrt |
Reversion Power (W) | int32 | RW | ❌ | 🔲 Untested | Read by read_control_status() only |
| 40324 | WSetPct |
Active Power Setpoint (%) | int16 | RW | ✅ | ✅ Working | send_command() step 2 — sign inverted (see quirks) |
| 40325 | WSetPctRvrt |
Reversion Power (%) | int16 | RW | ❌ | 🔲 Untested | Could set fallback power level |
| 40326 | WSetEnaRvrt |
Reversion Enable | enum16 | RW | ❌ | 🔲 Untested | What happens when command reverts |
| 40327 | WSetRvrtTms |
Reversion Timeout (s) | uint32 | RW | ⚠️ | ⚠️ Config only | Value accepted & persists, but countdown never activates (TESTED 2026-03-08) |
| 40329 | WSetRvrtRem |
Reversion Time Remaining (s) | uint32 | R | ✅ | ❌ Non-functional | Always 0 — countdown never activates despite WSetRvrtTms being set (TESTED 2026-03-08) |
[!IMPORTANT] Sign Convention Quirk:
WSetPctis inverted on FranklinWH hardware. Positive values = discharge (in standard SunSpec, positive = charge). The library inverts:m704.WSetPct.value = -pct_raw. SeeFRANKLINWH_SUNSPEC_QUIRKS.md.[!WARNING] WSet vs WSetPct: Writing to
WSet(absolute watts) alongsideWSetPctcauses mode flickering on the aGate. The library uses only WSetPct for all power commands.WSetis zeroed duringreset_control_state()but never used for active control.
Current Command Sequence:
1. STOP: WSetEna = 0 (disable control)
2. CONFIG: WSetMod = 0, WSetPct = -X (set mode, set power %)
3. ENABLE: WSetEna = 1 (enable control)
4. VERIFY: read WSetPct (confirm value took)
Code entry points:
- controller.py:send_command() — primary charge/discharge interface
- controller.py:reset_control_state() — stop and release control
- controller.py:read_control_status() — read all WSet fields
- controller.py:healthcheck() — zombie state detection
Max Power Limit (WMaxLim Group)¶
Not currently used by the library. Could limit max inverter power.
| Address | Field | Label | Type | RW | Current Value | Status |
|---|---|---|---|---|---|---|
| 40310 | WMaxLimPctEna |
Max Power Limit Enable | enum16 | RW | 0 (disabled) | ❌ Read-only (P2 tested) |
| 40311 | WMaxLimPct |
Max Power Limit (%) | uint16 | RW | 100% (raw: 1000) | ❌ Read-only (P2 tested) |
| 40312 | WMaxLimPctRvrt |
Reversion Limit (%) | uint16 | RW | None | ❌ Unimplemented |
| 40313 | WMaxLimPctEnaRvrt |
Reversion Enable | enum16 | RW | None | ❌ Unimplemented |
| 40314 | WMaxLimPctRvrtTms |
Reversion Timeout (s) | uint32 | RW | None | ❌ Unimplemented |
| 40316 | WMaxLimPctRvrtRem |
Reversion Time Remaining | uint32 | R | None | ❌ Unimplemented |
[!NOTE] Potential Use Case: Could be used to cap inverter output during grid-sensitive periods or to implement soft power ramp-down. Worth testing if the aGate respects this limit.
Power Factor Control (PFW Groups)¶
Not currently used by the library. Controls inverter power factor.
| Address | Field | Label | Type | RW | Current Value | Status |
|---|---|---|---|---|---|---|
| 40298 | PFWInjEna |
PF Enable (W Inject) | enum16 | RW | 1 (enabled!) | ✅ Writable — toggles actual PF |
| 40299 | PFWInjEnaRvrt |
PF Reversion Enable (Inj) | enum16 | RW | None | ❌ Unimplemented |
| 40300 | PFWInjRvrtTms |
PF Reversion Time (Inj) | uint32 | RW | None | ❌ Unimplemented |
| 40302 | PFWInjRvrtRem |
PF Rev Time Remaining | uint32 | R | None | ❌ Unimplemented |
| 40304 | PFWAbsEna |
PF Enable (W Absorb) | enum16 | RW | None | ❌ Unimplemented |
| 40305 | PFWAbsEnaRvrt |
PF Reversion Enable (Abs) | enum16 | RW | None | ❌ Unimplemented |
| 40306 | PFWAbsRvrtTms |
PF Reversion Time (Abs) | uint32 | RW | None | ❌ Unimplemented |
| 40308 | PFWAbsRvrtRem |
PF Rev Time Remaining | uint32 | R | None | ❌ Unimplemented |
[!WARNING]
PFWInjEna= 1 — Active and WRITABLE. When enabled (=1): M701 PF = -1 (≈ unity). When disabled (=0): PF = 3 (0.003, essentially no correction). Do not leave disabled — the aGate's default PF correction is applied through this register.
Reactive Power Control (VarSet Group)¶
Not currently used by the library. Controls reactive power (Var) output.
| Address | Field | Label | Type | RW | Current Value | Status |
|---|---|---|---|---|---|---|
| 40331 | VarSetEna |
Reactive Power Enable | enum16 | RW | 0 | ❌ Read-only (P3 tested) |
| 40332 | VarSetMod |
Reactive Power Mode | enum16 | RW | 1 | ❌ Read-only (pre-configured) |
| 40333 | VarSetPri |
Reactive Power Priority | enum16 | RW | 2 | ❌ Read-only (pre-configured) |
| 40334 | VarSet |
Reactive Power (Var) | int32 | RW | 0 | ❌ Read-only (P3 tested) |
| 40336 | VarSetRvrt |
Reversion Reactive Power | int32 | RW | None | ❌ Unimplemented |
| 40338 | VarSetPct |
Reactive Power (%) | int16 | RW | None | ❌ Unimplemented |
| 40339 | VarSetPctRvrt |
Reversion Reactive (%) | int16 | RW | None | ❌ Unimplemented |
| 40340 | VarSetEnaRvrt |
Reversion Enable | enum16 | RW | None | ❌ Unimplemented |
| 40341 | VarSetRvrtTms |
Reversion Timeout (s) | uint32 | RW | None | ❌ Unimplemented |
| 40343 | VarSetRvrtRem |
Reversion Time Remaining | uint32 | R | None | ❌ Unimplemented |
Ramp Rate Control¶
Not currently used. Controls how fast power ramps up/down.
| Address | Field | Label | Type | RW | Current Value | Status |
|---|---|---|---|---|---|---|
| 40345 | WRmp |
Normal Ramp Rate | uint16 | RW | None | ❌ Unimplemented (P4 tested) |
| 40346 | WRmpRef |
Ramp Rate Reference | enum16 | RW | None | ❌ Unimplemented (P4 tested) |
| 40347 | VarRmp |
Reactive Ramp Rate | uint16 | RW | None | ❌ Unimplemented |
Anti-Islanding¶
| Address | Field | Label | Type | RW | Current Value | Status |
|---|---|---|---|---|---|---|
| 40348 | AntiIslEna |
Anti-Islanding Enable | enum16 | RW | None | 🔲 Untested — DO NOT disable |
Scale Factors¶
| Address | Field | Value | Applied To |
|---|---|---|---|
| 40349 | PF_SF |
-3 | Power Factor (÷1000) |
| 40350 | WMaxLimPct_SF |
-1 | Max Power Limit (÷10) |
| 40351 | WSet_SF |
0 | Active Power (×1) |
| 40352 | WSetPct_SF |
-1 | Active Power % (÷10) |
| 40353 | VarSet_SF |
0 | Reactive Power (×1) |
| 40354 | VarSetPct_SF |
-1 | Reactive Power % (÷10) |
2. Model 715 — DER Control¶
Note: M715 registers use base-1 addressing (1087-1095) for raw Modbus TCP. Base-40000 addresses (41087+) return ILLEGAL_DATA_ADDRESS.
| Address (base-1) | Field | Label | Type | RW | Current Value | Status | Notes |
|---|---|---|---|---|---|---|---|
| 1089 | LocRemCtl |
Control Mode | enum16 | ❌ R | 1 (Local) | ❌ Read-only | SunSpec says writable — aGate ignores (see LocRemCtl Paradox) |
| 1090 | DERHb |
DER Heartbeat | uint32 | R | 0 | ❌ Non-functional | Always 0 — DER heartbeat protocol not implemented (TESTED) |
| 1092 | ControllerHb |
Controller Heartbeat | uint32 | RW* | 0 | ❌ Non-functional | Write accepted, value silently ignored (TESTED via sunspec2 + raw TCP) |
| 1094 | AlarmReset |
Alarm Reset | uint16 | RW | 0 | 🔲 Untested | Used in check_blocking_alarms() but never written |
| 1095 | OpCtl |
Set Operation | enum16 | RW | 0 | 🔲 Untested | Start/Stop/Standby the DER |
[!CAUTION] LocRemCtl Paradox: Per SunSpec 2,
LocRemCtl=1(Local) should mean the DER rejects ALL client writes. FranklinWH violates this — M704 power writes work, but lifecycle features (heartbeat, reversion countdown) are non-functional. This is a selective-write hybrid unique to FranklinWH. SeeFRANKLINWH_SUNSPEC_QUIRKS.mdfor full analysis.[!IMPORTANT] ControllerHb TESTED 2026-03-08: Writes accepted at protocol level (no Modbus exception) via both sunspec2 model.write() and raw TCP func 16 at address 1092. Value stays 0 after all writes. aGate firmware silently discards heartbeat writes. See
tests/results/2026-03-08_p1_control_tests.md.
3. FranklinWH Extension Controls (15507–15509)¶
| Address | Field | Label | Type | RW | Current Value | Status | Notes |
|---|---|---|---|---|---|---|---|
| 15507 | OnGridMode |
Operating Mode | uint16 | R* | 2 (Self-Consumption) | ✅ Read works | ❌ Write blocked without SPAN |
| 15508 | SelfReserve |
Self-Consumption Reserve | uint16 | R* | 20 (%) | ✅ Read works | ❌ Write blocked without SPAN |
| 15509 | TouReserve |
TOU Reserve | uint16 | R* | 20 (%) | ✅ Read works | ❌ Write blocked without SPAN |
*R = Read-only without SPAN Modbus unlock. See FRANKLINWH_SUNSPEC_QUIRKS.md "Write Access Asymmetry".
4. Test Status Summary¶
| Category | Registers | Status | Evidence |
|---|---|---|---|
| Battery Power (WSetPct) | 40318-40324 | ✅ Working | Live verified — charge/discharge confirmed |
| WSet (absolute watts) | 40320 | ⚠️ Avoided | Causes flickering when used with WSetPct |
| Reversion Config (WSetRvrtTms) | 40327 | ⚠️ Config only | Value accepted & persists, but countdown never activates (TESTED) |
| Reversion Countdown (WSetRvrtRem) | 40329 | ❌ Non-functional | Always 0 — hardware timer not implemented (TESTED) |
| Reversion Power | 40322/40325 | 🔲 Untested | Could set fallback power after timeout |
| Max Power Limit | 40310-40316 | 🔲 Untested | Could cap inverter output |
| Power Factor | 40298-40308 | ✅ PFWInjEna writable | PFWInjEna toggles actual PF. All Rvrt regs unimplemented |
| Reactive Power | 40331-40343 | ❌ Read-only | VarSetEna/VarSet writes silently discarded |
| Ramp Rates | 40345-40347 | 🔲 Untested | Could smooth power transitions |
| Controller Heartbeat | 1092 | ❌ Non-functional | Write accepted, value silently ignored (TESTED) |
| DER Heartbeat | 1090 | ❌ Non-functional | Always 0 (TESTED) |
| OpCtl (Start/Stop) | 1095 | 🔲 Untested | Could start/stop DER |
| LocRemCtl | 1089 | ❌ Read-only | SunSpec handoff not supported; LocRemCtl Paradox (TESTED) |
| Extension Mode/Reserves | 15507-15509 | ❌ Write blocked | Need SPAN Modbus unlock |
5. Future Test Plan¶
Priority 1 — Command Persistence and Keep-Alive — TESTED 2026-03-08¶
| Test | What to Do | Result | Evidence |
|---|---|---|---|
| Write ControllerHb | Write incrementing counter to 1092 | ❌ Non-functional — writes silently ignored | Test 1a/1b/1c |
| Set WSetRvrtTms | Write timeout to 40327, issue command | ⚠️ Config only — value persists but countdown never activates | Test 2a/2b/2c |
| Monitor WSetRvrtRem | Read countdown after timeout set | ❌ Always 0 — hardware timer not implemented | Test 2b/2c |
[!IMPORTANT] Root cause: LocRemCtl Paradox. Both features are SunSpec 2 remote controller lifecycle features that require
LocRemCtl=0(Remote). The aGate keeps this at 1 (Local) and read-only, selectively accepting power writes but ignoring lifecycle features. Software-side watchdog must be implemented instead. SeeFRANKLINWH_SUNSPEC_QUIRKS.md.
Priority 2 — Power Limiting and Ramp Control¶
| Test | What to Do | Expected Outcome | Risk |
|---|---|---|---|
| WMaxLimPct | Write 50% limit to 40311, enable at 40310 | Inverter caps at 50% rated power | Medium — limits output |
| WRmp ramp rate | Write ramp value to 40345 | Power changes gradually instead of step | Low |
| VarSet reactive | Enable VarSet, write small Var value | Observe reactive power output | Low |
Priority 3 — Power Factor Investigation¶
| Test | What to Do | Expected Outcome | Risk |
|---|---|---|---|
| Read PF target | Investigate what PFWInjEna=1 is doing | Understand default PF behavior | None — read only |
| Disable PFWInjEna | Write PFWInjEna=0, observe PF change | Determine if PF control is enforced | Medium |
Priority 4 — DER Operations¶
| Test | What to Do | Expected Outcome | Risk |
|---|---|---|---|
| OpCtl start/stop | Write OpCtl values to 41095 | May start/stop the inverter | ⚠️ High — could disconnect |
| AlarmReset | Write to 41094 after a fault | Clear fault condition | Low |
6. Tool Commands for Testing¶
# Read all M704 control registers (ground truth)
python3 tools/modbus_sunspec2_reader.py -i YOUR_AGATE_IP -t 10 -dvalues --vals | grep -A 50 'Model 704'
# Read M715 control registers
python3 tools/modbus_sunspec2_reader.py -i YOUR_AGATE_IP -t 10 -dvalues --vals | grep -A 12 'Model 715'
# Read extension mode/reserves
python3 tools/modbus_sunspec2_reader.py -i YOUR_AGATE_IP -t 10 --raw 15507:3
# Library: read control status
python3 -c "
from franklinwh_modbus import FranklinWHController
ctrl = FranklinWHController('YOUR_AGATE_IP')
ctrl.connect()
print(ctrl.read_control_status())
print(ctrl.read_native_mode())
ctrl.disconnect()
"
# Library: healthcheck (includes zombie detection)
python3 -c "
from franklinwh_modbus import FranklinWHController
ctrl = FranklinWHController('YOUR_AGATE_IP')
ctrl.connect()
h = ctrl.healthcheck()
print(f'Healthy: {h.healthy}')
print(f'Message: {h.message}')
for k,v in h.details.items(): print(f' {k}: {v}')
for r in h.recommendations: print(f' ⚠️ {r}')
ctrl.disconnect()
"
7. Retrospective: Why Past Tests Failed¶
[!NOTE] Previous attempts at battery control were largely "winging it" — writing to registers without understanding: - Correct addresses — legacy code used PDU offsets (317, 318, 319) instead of absolute addresses (40318, 40319, 40320) - Sign conventions — WSetPct is inverted on FranklinWH hardware (positive=discharge, not charge) - Sequencing requirements — M704 requires a multi-step STOP→CONFIG→ENABLE→VERIFY sequence; single writes are ignored - WSet vs WSetPct conflict — writing both causes mode flickering; must use only WSetPct - FranklinWH quirks — LocRemCtl handoff not supported, extension registers read-only
This document now provides the correct baseline for future formal use-case testing of each register group.
8. Future TODO: FranklinWH App Parity¶
Goals for feature parity with the FranklinWH mobile app:
- [ ] Manual Controls — mimic the app's manual charge/discharge with power level selection (partially working via
send_command()) - [ ] TOU Scheduling — implement time-of-use schedule programming (requires extension register 15507 write access OR virtual mode emulation)
- [ ] Max Power Settings — the app allows setting max charge/discharge power limits (map to WMaxLimPct? or extension registers?)
- [ ] Custom Inverter/Grid Operations — app offers grid export limits and inverter power caps (map to VarSet or WMaxLim groups?)
- [ ] Reserve Level Management — app sets Self-Consumption and TOU reserve percentages (extension registers 15508-15509, currently read-only)
[!IMPORTANT] Achieving full App parity likely requires the SPAN Modbus unlock for extension register writes. Without it, we can emulate some behaviors via M704 power commands, but cannot natively change modes or reserves.
Last Updated: 2026-03-08 (P1 tests complete)
Test Results: tests/results/2026-03-08_p1_control_tests.md
See also: ORCHESTRATION_AND_CONTROL.md for command sequencing diagrams