npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@enyo-energy/sunspec-sdk

v0.0.80

Published

enyo Energy Sunspec SDK

Readme

enyo SunSpec SDK

SunSpec Modbus client for reading data from solar inverters, batteries, meters, and other energy devices over Modbus TCP/RTU.

Table of Contents


Appliance Manager Integration

SunspecInverter, SunspecBattery, and SunspecMeter take an ApplianceManager from @enyo-energy/energy-app-sdk and call createOrUpdateAppliance with the device's networkDevice plus the common block's serialNumber (which may be empty for some firmware revisions).

Since @enyo-energy/energy-app-sdk 0.0.135 the default identifier strategy is SerialNumberStrategy. If a device's common block has no serial number, createOrUpdateAppliance throws MissingIdentifierError and the SDK logs the error and skips appliance creation.

Consumers whose devices may not expose a serial number should configure a fallback when initializing the ApplianceManager, e.g.:

import { ApplianceManager, FallbackIdentifierStrategy, SerialNumberStrategy, HostnameStrategy } from '@enyo-energy/energy-app-sdk';

const applianceManager = await ApplianceManager.initialize(energyApp, {
    identifierStrategy: new FallbackIdentifierStrategy([
        new SerialNumberStrategy(),
        new HostnameStrategy(),
    ]),
});

This SDK does not configure the strategy itself — it is the consumer app's responsibility.


Battery Feature Modes

SunspecBattery populates appliance.battery.features for every connected battery — the list the host's topology / UI reads to decide which control surfaces to render. The consumer chooses how that list is computed by passing a required featureMode argument to the SunspecBattery constructor. There is no default; the choice is explicit so a SunSpec device whose register surface lies (or whose responsiveness is unknown) cannot silently turn into an actionable battery from the host's point of view.

Three kinds are available, exposed via the SunspecBatteryFeatureModeKind enum:

| Kind | Calibration allowed? | What appears in appliance.battery.features | |---|---|---| | Disabled | No (configureCalibration throws) | Exactly the allowedFeatures array the consumer passes — register state is ignored. | | RegisterBased | No (configureCalibration throws) | Features the SunSpec registers expose. Filtered down to allowedFeatures if that's set. | | CalibrationBased | Yes (required for controllable features to ever appear) | Same as RegisterBased for read-only features. The four controllable features stay hidden until the calibration result store reports the appliance as calibrated. |

The four features the SDK treats as controllable (the ones calibration gates):

  • EnyoBatteryFeature.GridCharging
  • EnyoBatteryFeature.GridDischarging
  • EnyoBatteryFeature.ChargeLimitation
  • EnyoBatteryFeature.DischargeLimitation

Every other EnyoBatteryFeature is read-only from the SDK's perspective and is published as soon as the corresponding register is present (in RegisterBased and CalibrationBased) or as soon as the consumer lists it (in Disabled).

Disabled

Picks no registers, runs no calibration. Whatever the consumer lists in allowedFeatures becomes the published list; omit it (or pass []) to publish nothing.

Use when the host has out-of-band knowledge of the battery's capabilities — e.g. a curated commissioning database — and doesn't want the SDK inferring anything from registers it may not trust. Also useful for read-only deployments: omit allowedFeatures and the host will treat the battery as observation-only.

import { SunspecBattery, SunspecBatteryFeatureModeKind } from '@enyo-energy/sunspec-sdk';
import { EnyoBatteryFeature } from '@enyo-energy/energy-app-sdk';

const observationOnly = new SunspecBattery(
    app, name, networkDevice, client, applianceManager,
    { kind: SunspecBatteryFeatureModeKind.Disabled },
    // → appliance.battery.features = []
);

const curated = new SunspecBattery(
    app, name, networkDevice, client, applianceManager,
    {
        kind: SunspecBatteryFeatureModeKind.Disabled,
        allowedFeatures: [EnyoBatteryFeature.GridCharging],
    },
    // → appliance.battery.features = ['grid-charging'], regardless of what the registers say
);

Calling configureCalibration(...) on a Disabled battery throws.

RegisterBased

The SDK looks at the SunSpec Model 124 registers and publishes the features whose underlying registers are present:

  • chaGriSet register present → EnyoBatteryFeature.GridCharging
  • wChaMax register present → EnyoBatteryFeature.ChargeLimitation

Pass allowedFeatures to intersect the detected set with a whitelist — useful for hiding features the device exposes but the host doesn't want exercised. The allow-list never adds features that the registers don't expose; it is strictly a filter.

const trustRegisters = new SunspecBattery(
    app, name, networkDevice, client, applianceManager,
    { kind: SunspecBatteryFeatureModeKind.RegisterBased },
    // → appliance.battery.features reflects whichever Model 124 controls are exposed
);

const trustRegistersButHideGridCharging = new SunspecBattery(
    app, name, networkDevice, client, applianceManager,
    {
        kind: SunspecBatteryFeatureModeKind.RegisterBased,
        allowedFeatures: [EnyoBatteryFeature.ChargeLimitation],
    },
    // → publishes ChargeLimitation only (even if chaGriSet is present)
);

Calling configureCalibration(...) on a RegisterBased battery throws.

CalibrationBased

Same register-driven detection as RegisterBased for read-only features, but the four controllable features stay stripped from the published list until CalibrationResultStore.isCalibrated(applianceId) returns true for this battery. Once the verdict flips, the next readData() cycle republishes the full set.

configureCalibration(...) must be called after connect() — without it, controllable features stay hidden forever (safe-fallback behaviour).

const verified = new SunspecBattery(
    app, name, networkDevice, client, applianceManager,
    { kind: SunspecBatteryFeatureModeKind.CalibrationBased },
);
await verified.connect();
verified.configureCalibration({ resultStore });   // see "Battery Calibration" below
trigger.register(verified.getBatteryCalibrator()!);
// Until trigger.tick() runs a successful test charge, controllable features are absent.
// After it succeeds, they reappear within one readData() cycle.

See Battery Calibration for the full wiring of resultStore, trigger, and configureCalibration config.

Choosing a mode

| Situation | Pick | |---|---| | Battery is observation-only, or the host already knows its capabilities | Disabled | | Trust the SunSpec register surface; no responsiveness verification needed | RegisterBased | | Want a test charge to confirm the battery actually reacts before exposing controls | CalibrationBased |

The mode is fixed for the lifetime of the SunspecBattery instance. To switch modes, construct a new instance.


Battery Calibration

The SDK integrates @enyo-energy/appliance-calibration to verify that a battery actually responds to control commands before the host action-taker is allowed to send it any. The verification is a short test charge wrapped in a snapshot/restore so a hung or crashed run never leaves writable registers in a half-modified state, and the verdict (calibrated / not-supported / failed) is persisted per appliance so it survives restarts.

The SDK owns the SunSpec-specific pieces — the snapshot service, the vendor driver, the per-battery BatteryCalibrator. The consumer owns the trigger and the result store, so scheduling, multi-tenant storage namespacing, and command gating stay configurable.

Architecture

| Piece | Lives in | Who builds it | |---|---|---| | SunspecCalibrationStorage (adapter over EnergyAppStorage) | this SDK | consumer (factory: createSunspecCalibrationStorage(app)) | | CalibrationResultStore (persisted verdict map) | appliance-calibration | consumer — one shared instance across batteries | | SnapshotService<SunspecBatteryControls> | appliance-calibration | SDK (per-battery, inside SunspecBattery) | | SunspecBatteryCalibrationDriver | this SDK | SDK (per-battery, inside SunspecBattery) | | BatteryCalibrator<SunspecBatteryControls> | appliance-calibration | SDK — built by configureCalibration, exposed via getBatteryCalibrator | | CalibrationTrigger | appliance-calibration | consumer — drives runCalibration() on its own schedule |

Feature mode

Calibration is only relevant when SunspecBattery is constructed with SunspecBatteryFeatureModeKind.CalibrationBased — that's the mode under which the SDK gates controllable features on the calibration verdict. See Battery Feature Modes above for the full mode discussion; the rest of this section assumes the calibration-based mode has been selected.

Consumer wiring

import {
    SunspecBattery,
    SunspecBatteryFeatureModeKind,
    // re-exported from @enyo-energy/appliance-calibration so you don't need a second import:
    CalibrationResultStore,
    CalibrationTrigger,
    createSunspecCalibrationStorage,
} from '@enyo-energy/sunspec-sdk';

// 1. One storage adapter + one result store for the whole app — sharing the result store
//    across batteries keeps the persisted verdict map from being clobbered by per-instance writes.
const storage     = createSunspecCalibrationStorage(app);
const resultStore = new CalibrationResultStore(storage);
await resultStore.initialize();

// 2. One trigger that fans out across every registered calibrator.
const trigger = new CalibrationTrigger({ resultStore });

// 3. Per battery: build it with the desired feature mode, then connect,
//    then (only in calibration-based mode) configureCalibration + register.
for (const config of batteryConfigs) {
    const battery = new SunspecBattery(
        app, name, networkDevice, client, applianceManager,
        { kind: SunspecBatteryFeatureModeKind.CalibrationBased },
        // unitId, port, baseAddress, ...
    );
    await battery.connect();
    battery.configureCalibration({
        resultStore,
        // Optional — override any BatteryCalibratorConfig defaults (see "Tuning" below).
        config: { testPowerW: 300 },
    });
    const calibrator = battery.getBatteryCalibrator();
    if (calibrator) {
        trigger.register(calibrator);
    }
}

// 4. Drive the trigger on your own cadence. tick() runs at most one calibration per call
//    and is a no-op for any battery that already has a stored result.
client.useInterval().createInterval('30m', () => trigger.tick());

Gate commands on the calibration result

In whatever code emits battery commands, check the store synchronously before sending:

if (!resultStore.isCalibrated(applianceId)) {
    console.log(`Skipping ${applianceId}: not calibrated yet`);
    return;
}
// ...send the command

Per-feature probes

In calibration-based mode the SDK runs SunspecBatteryFeatureCalibrator (a subclass of the library's BatteryCalibrator) which exercises each of the four controllable features individually and records the outcomes in CalibrationResult.notes as JSON. Only features whose probes pass land in appliance.battery.features after calibration.

The session is one snapshot → four probes → one restore. Probes run in this order so the battery has energy to discharge with by the end:

| # | Probe | What it writes | Pass condition | |---|---|---|---| | 1 | GridCharging | chaGriSet=GRID, wChaMax=testPowerW, inWRte=100, storCtlMod=CHARGE | Battery power AND grid power both rise above responseThresholdW | | 2 | ChargeLimitation | same as above with inWRte=50 | Charge power settles within ±responseThresholdW of testPowerW × 0.5 | | 3 | DischargeLimitation | chaGriSet=PV, outWRte=50, storCtlMod=DISCHARGE | Discharge power settles within ±responseThresholdW of testPowerW × 0.5 | | 4 | GridDischarging | chaGriSet=PV, outWRte=100, storCtlMod=DISCHARGE | Battery discharges AND meter shows reverse flow above responseThresholdW |

Each probe runs a writeBatteryControls(...) then polls the driver's cached battery/grid power until either the predicate passes or responseTimeoutMs elapses. Between probes the SDK resets to a neutral state (storCtlMod=AUTO, chaGriSet=PV, inWRte/outWRte=100, wChaMax=baseline) so each probe starts cleanly. The final restoreFromSnapshot (run by the SnapshotService on stopCalibration) writes the original pre-calibration register set back regardless of outcome.

SoC preconditions

Probes refuse to run outside a useful SoC band and return not-supported instead — distinct from failed so the host can tell "device won't" apart from "we couldn't even try":

  • Charge probes (GridCharging, ChargeLimitation) need headroom: SoC ≥ 90% → not-supported.
  • Discharge probes (DischargeLimitation, GridDischarging) need energy: SoC ≤ 20% → not-supported.

Aggregate verdict

The library's CalibrationResult.state (one value per appliance) is computed from the four per-feature verdicts:

| Per-feature verdicts | CalibrationResult.state | |---|---| | At least one passed | calibrated | | All not-supported | not-supported | | Otherwise (mix of failed / not-supported) | failed |

isCalibrated(applianceId) therefore reads as "the SDK probed this device and at least something is controllable" — keep using it as the broad command-emission gate. The per-feature breakdown stored in notes is what resolveAdvertisedFeatures consults to decide which controllable features to publish; decodeFeatureResults(result.notes) returns the passing set and is exported for consumers that want to inspect it directly.

Other operational notes

  • Republishing cadence: the controllable features reappear on the next readData() cycle after the calibrator persists its result — not synchronously when calibration completes. With a typical 10s/30s/1m poll cadence the appliance metadata follows within one cycle.
  • Defence in depth: the feature filter is complementary to the isCalibrated command-gate, not a substitute. The consumer's action-taker should still check resultStore.isCalibrated(applianceId) synchronously before every command write — the filter keeps the controllable surface off the appliance until the device is verified, but a race window exists between calibration completing and the next readData cycle.
  • GridDischarge ambiguity: SunSpec Model 124 has no register to force battery output toward the grid versus toward local load. The probe drives the battery into discharge and then watches the meter for export. If the host's local load eats the discharge there will be no export and the probe records failed with a note explaining the likely cause. Hosts that know their grid topology can map this to "controllable, but couldn't be verified".

What configureCalibration does under the hood

For each battery it builds:

  • A SunspecBatteryCalibrationDriver that:
    • Snapshots writable controls via readBatteryControls().
    • Restores them via writeBatteryControls() (whitelist: storCtlMod, chaGriSet, wChaMax, inWRte, outWRte, minRsvPct).
    • Runs the test charge with the same 3-step sequence as handleStartGridCharge (enableGridCharging(true), wChaMax = powerW, setStorageMode(CHARGE)).
    • Reads battery power from a LatestValueCache fed by SunspecBattery.readData() each cycle.
    • Reads grid power from a LatestValueCache fed by a MeterValuesUpdateV1 data-bus subscription that's live for the driver's lifetime (so the grid signal works regardless of where the meter actually comes from).
  • A BatteryCalibrator<SunspecBatteryControls> wired to the driver, the per-battery SnapshotService, and the shared resultStore.

Battery disconnect() tears the driver subscription down.

What still happens through the data bus

The SDK also keeps listening for StartCalibrationV1 / StopCalibrationV1 data-bus messages so an external orchestrator (e.g. the action-taker) can begin and end a manual calibration session. Those handlers now drive the same SnapshotService<T> instance from @enyo-energy/appliance-calibration. While a manual session is active, any concurrent SetInverterFeedInLimitV1 / SetStorageChargeLimitV1 / etc. call records the field it touched into modifiedFields, and only those fields are written back on stop (or on the 5-minute auto-stop). This works exactly as before — the change is purely the implementation underneath.

Re-running a calibration

The trigger runs each battery once — appliances already marked calibrated, not-supported, or failed are skipped on every subsequent tick(). To force a re-run, drop the persisted result for that key:

await app.useStorage().remove('battery-calibration');   // wipes ALL results (the default key)

If you need targeted invalidation, snapshot the store via resultStore.snapshot(), mutate the map, and re-save the survivors. Periodic re-calibration is a host concern — build it on top of calibrator.runCalibration() directly rather than going through CalibrationTrigger.

Tuning

Override defaults via configureCalibration({ resultStore, config }):

| Key | Default | When to change | |---|---|---| | testPowerW | 500 W | Smaller batteries; PV-only sites where 500 W is visible. | | responseThresholdW | 200 W | Noisier meters; tighter tolerance for known-good devices. | | ackTimeoutMs | 10 s | Slow Modbus links. | | responseTimeoutMs | 20 s | Devices that take longer to ramp. | | pollIntervalMs | 2 s | Faster polling for short-window tests. |

See @enyo-energy/appliance-calibration's README for the full crash-safety contract and every result-store / snapshot-service knob.


Storage Schedule Control

SunspecBattery reacts to a single data-bus command for control: EnyoDataBusSetStorageScheduleV1. The message carries a mode (auto or schedule) and, when mode is schedule, a sorted relativeSchedule of {seconds, direction, powerW} entries. Subscription is automatic — each battery wires its own SunspecBatteryScheduleHandler (extending the SDK's StorageScheduleHandler from @enyo-energy/energy-app-sdk) during connect(). There is no consumer setup beyond connecting the battery.

What the SDK does on receipt

  1. Acks the message with Accepted immediately ("received & queued" — schedule entries play out over time).
  2. Snapshots the writable Model 124 registers (storCtlMod, chaGriSet, wChaMax, inWRte, outWRte) and persists them to EnergyAppStorage for restart-safe rollback.
  3. Activates the first entry immediately, then advances to subsequent entries as their seconds offsets elapse on a 1-second tick (driven by EnergyApp.useInterval()).
  4. Each Charge entry writes chaGriSet=GRID, inWRte = powerW / installedWChaMax × 100, storCtlMod=CHARGE.
  5. Each Discharge entry writes chaGriSet=PV, outWRte = powerW / installedWChaMax × 100, storCtlMod=DISCHARGE.
  6. Restores the snapshotted pre-schedule registers (storCtlMod, chaGriSet, wChaMax, inWRte, outWRte) only on mode: auto, disconnect(), or process-restart recovery — not when one mode: schedule is replaced by another. Schedule-to-schedule replacement keeps the device on the active register set and lets the new schedule's first entry take over directly, so consecutive schedules don't leak a storCtlMod=AUTO (0x3) write between them. The pre-schedule snapshot stays sticky across every replacement until one of the three terminal events fires.

Register write order

writeBatteryControls issues each register write sequentially in this order so the device never sees a stale-parameter window when the control mode changes: chaGriSet → wChaMax → inWRte → outWRte → minRsvPct → storCtlMod. Source pin and the limit/rate parameters land first; the control mode is written last so the device only "starts acting" once every governing value is fresh.

Power cap

Scheduled powerW is clamped to the device's installed wChaMax because the handler holds that value stable as the denominator for the percentage math throughout the schedule. To request more than the installed maximum, raise wChaMax out-of-band before sending the schedule.

Calibration coexistence

Each per-entry write is recorded into the calibration SnapshotService (when calibration is configured), so a calibration that starts mid-schedule still restores the right register set on stop / auto-stop.

Legacy messages

The earlier StartStorageGridChargeV1 / StopStorageGridChargeV1 / SetStorageChargeLimitV1 / SetStorageDischargeLimitV1 family is no longer handled by this SDK. Their type defs in @enyo-energy/energy-app-sdk are flagged @deprecated with pointers to SetStorageScheduleV1. Migrate any callers that still emit them.


How Addressing Works

Base Address Detection

SunSpec devices expose a "SunS" identifier at a known base address. The SDK auto-detects:

| Mode | Base Address | "SunS" Location | First Model Address | |------|-------------|-------------------|---------------------| | 1-based (most common) | 40001 | 40001–40002 | 40003 | | 0-based | 40000 | 40000–40001 | 40002 | | Custom | user-provided | user-provided | base + 2 |

Model Discovery

Starting from the first model address, the SDK scans through contiguous model blocks:

┌──────────────┬──────────────┬─────────────────────────┐
│ Model ID     │ Model Length │ Model Data Registers    │
│ (1 register) │ (1 register) │ (Length registers)      │
├──────────────┼──────────────┼─────────────────────────┤
│ offset 0     │ offset 1     │ offset 2 ... Length+1   │
└──────────────┴──────────────┴─────────────────────────┘
         ▲                              ▲
    model.address               model.address + 2
  • model.address — Modbus address of the model ID register (start of the 2-register header)
  • model.length — Number of data registers (does NOT include the 2-register header)
  • Next model — Located at model.address + 2 + model.length
  • End marker — Model ID 0xFFFF (65535) signals end of model chain

Register Offsets

All register offsets in this document are relative to model.address:

  • Offset 0 = Model ID register
  • Offset 1 = Model Length register
  • Offset 2 = First data register (for most models)

Bulk Register Reading

Each model is read in a single Modbus call via readModelBlock(), which reads model.length + 2 contiguous registers starting at model.address. Individual fields are then extracted from the resulting buffer — no additional network round trips.


Data Types

| Type | Registers | Bytes | Description | |------|-----------|-------|-------------| | uint16 | 1 | 2 | Unsigned 16-bit integer | | int16 | 1 | 2 | Signed 16-bit integer (two's complement) | | uint32 | 2 | 4 | Unsigned 32-bit integer (big-endian) | | acc32 | 2 | 4 | 32-bit accumulator (unsigned, monotonically increasing) | | string | N | 2N | UTF-8 string, null-padded | | enum16 | 1 | 2 | Enumerated value (uint16) | | enum32 | 2 | 4 | Enumerated value (uint32) | | bitfield16 | 1 | 2 | Bit flags (uint16) | | bitfield32 | 2 | 4 | Bit flags (uint32) |

NOT_IMPLEMENTED Sentinel Values

Registers may return sentinel values indicating the field is not implemented:

| Type | Sentinel Value | Hex | |------|---------------|-----| | int16 | -32768 | 0x8000 | | uint16 | 65535 | 0xFFFF | | int32 | -2147483648 | 0x80000000 | | uint32 | 4294967295 | 0xFFFFFFFF | | enum16 / bitfield16 | 65535 | 0xFFFF | | enum32 / bitfield32 | 4294967295 | 0xFFFFFFFF | | acc16 | 0 | 0x0000 (NOT_ACCUMULATED) | | acc32 | 0 | 0x00000000 (NOT_ACCUMULATED) | | string | all NULLs | 0x0000 per register |


Scale Factors

Many numeric fields have an associated scale factor (SF) register. The final value is:

scaled_value = raw_value × 10^SF

Scale factor registers are int16 and typically contain values like -2, -1, 0, 1, 2.

For example: raw value 2345 with SF -12345 × 10⁻¹ = 234.5


Model 1 — Common Block

Method: readCommonBlock()

| Offset | Field | Type | Qty | Description | |--------|-------|------|-----|-------------| | 0 | ID | uint16 | 1 | Model ID (= 1) | | 1 | L | uint16 | 1 | Model length (typically 65–66) | | 2–17 | Mn | string | 16 | Manufacturer name | | 18–33 | Md | string | 16 | Model name | | 34–41 | Opt | string | 8 | Options | | 42–49 | Vr | string | 8 | Firmware version | | 50–65 | SN | string | 16 | Serial number | | 66 | DA | uint16 | 1 | Modbus device address |


Model 101 — Single-Phase Inverter

Method: readSinglePhaseInverterData()

Scale Factors

| Offset | Field | Type | Description | |--------|-------|------|-------------| | 6 | A_SF | int16 | Current SF | | 10 | W_SF | int16 | Power SF | | 12 | Hz_SF | int16 | Frequency SF | | 13 | V_SF | int16 | Voltage SF | | 18 | DCA_SF | int16 | DC current SF | | 19 | DCV_SF | int16 | DC voltage SF | | 21 | DCW_SF | int16 | DC power SF |

Data Registers

| Offset | Field | Type | SF | Description | |--------|-------|------|----|-------------| | 2 | A | uint16 | A_SF | AC current (A) | | 7 | PhVphA | uint16 | V_SF | Phase voltage AN (V) | | 9 | W | int16 | W_SF | AC power (W) | | 11 | Hz | uint16 | Hz_SF | Frequency (Hz) | | 14 | DCA | uint16 | DCA_SF | DC current (A) | | 15 | DCV | uint16 | DCV_SF | DC voltage (V) | | 20 | DCW | int16 | DCW_SF | DC power (W) | | 24 | St | uint16 | — | Operating state (enum) |


Model 103 — Three-Phase Inverter

Method: readInverterData()

Scale Factors

| Offset | Field | Type | Description | |--------|-------|------|-------------| | 6 | A_SF | int16 | Current SF | | 13 | V_SF | int16 | Voltage SF | | 15 | W_SF | int16 | Power SF | | 17 | Hz_SF | int16 | Frequency SF | | 19 | VA_SF | int16 | Apparent power SF | | 21 | VAr_SF | int16 | Reactive power SF | | 23 | PF_SF | int16 | Power factor SF | | 26 | WH_SF | int16 | Energy SF | | 28 | DCA_SF | int16 | DC current SF | | 29 | DCV_SF | int16 | DC voltage SF | | 31 | DCW_SF | int16 | DC power SF | | 36 | Tmp_SF | int16 | Temperature SF |

Data Registers

| Offset | Field | Type | SF | Description | |--------|-------|------|----|-------------| | 2 | A | uint16 | A_SF | Total AC current (A) | | 3 | AphA | uint16 | A_SF | Phase A current (A) | | 4 | AphB | uint16 | A_SF | Phase B current (A) | | 5 | AphC | uint16 | A_SF | Phase C current (A) | | 7 | PPVphAB | uint16 | V_SF | Line voltage AB (V) | | 8 | PPVphBC | uint16 | V_SF | Line voltage BC (V) | | 9 | PPVphCA | uint16 | V_SF | Line voltage CA (V) | | 10 | PhVphA | uint16 | V_SF | Phase voltage AN (V) | | 11 | PhVphB | uint16 | V_SF | Phase voltage BN (V) | | 12 | PhVphC | uint16 | V_SF | Phase voltage CN (V) | | 14 | W | int16 | W_SF | AC power (W) | | 16 | Hz | uint16 | Hz_SF | Frequency (Hz) | | 18 | VA | uint16 | VA_SF | Apparent power (VA) | | 20 | VAr | int16 | VAr_SF | Reactive power (VAr) | | 22 | PF | int16 | PF_SF | Power factor | | 24–25 | WH | acc32 | WH_SF | Cumulative energy (Wh) | | 27 | DCA | uint16 | DCA_SF | DC current (A) | | 28 | DCV | uint16 | DCV_SF | DC voltage (V) | | 30 | DCW | int16 | DCW_SF | DC power (W) | | 32 | TmpCab | int16 | Tmp_SF | Cabinet temperature (°C) | | 34 | TmpSnk | int16 | Tmp_SF | Heat sink temperature (°C) | | 35 | TmpTrns | int16 | Tmp_SF | Transformer temperature (°C) | | 36 | TmpOt | int16 | Tmp_SF | Other temperature (°C) | | 38 | St | uint16 | — | Operating state (enum) | | 39 | StVnd | uint16 | — | Vendor-specific state | | 40–41 | Evt1 | bitfield32 | — | Event flags 1 | | 42–43 | Evt2 | bitfield32 | — | Event flags 2 | | 44–45 | EvtVnd1 | bitfield32 | — | Vendor events 1 | | 46–47 | EvtVnd2 | bitfield32 | — | Vendor events 2 | | 48–49 | EvtVnd3 | bitfield32 | — | Vendor events 3 | | 50–51 | EvtVnd4 | bitfield32 | — | Vendor events 4 |


Model 121 — Inverter Settings

Method: readInverterSettings()

Scale Factors

| Offset | Field | Type | Description | |--------|-------|------|-------------| | 22 | WMax_SF | int16 | Power SF | | 23 | VRef_SF | int16 | Voltage reference SF | | 24 | VRefOfs_SF | int16 | Voltage offset SF | | 25 | VMinMax_SF | int16 | Min/max voltage SF | | 26 | VAMax_SF | int16 | Apparent power SF | | 27 | VArMax_SF | int16 | Reactive power SF | | 28 | WGra_SF | int16 | Ramp rate SF | | 29 | PFMin_SF | int16 | Power factor SF | | 30 | MaxRmpRte_SF | int16 | Max ramp rate SF | | 31 | ECPNomHz_SF | int16 | Frequency SF |

Data Registers

| Offset | Field | Type | SF | Description | |--------|-------|------|----|-------------| | 2 | WMax | uint16 | WMax_SF | Maximum power output (W) | | 3 | VRef | uint16 | VRef_SF | Voltage at PCC (V) | | 4 | VRefOfs | int16 | VRefOfs_SF | Voltage offset from PCC (V) | | 5 | VMax | uint16 | VMinMax_SF | Maximum voltage (V) | | 6 | VMin | uint16 | VMinMax_SF | Minimum voltage (V) | | 7 | VAMax | uint16 | VAMax_SF | Maximum apparent power (VA) | | 8 | VArMaxQ1 | int16 | VArMax_SF | Max reactive power Q1 (VAr) | | 9 | VArMaxQ2 | int16 | VArMax_SF | Max reactive power Q2 (VAr) | | 10 | VArMaxQ3 | int16 | VArMax_SF | Max reactive power Q3 (VAr) | | 11 | VArMaxQ4 | int16 | VArMax_SF | Max reactive power Q4 (VAr) | | 12 | WGra | uint16 | WGra_SF | Default ramp rate (%WMax/sec) | | 13 | PFMinQ1 | int16 | PFMin_SF | Min power factor Q1 | | 14 | PFMinQ2 | int16 | PFMin_SF | Min power factor Q2 | | 15 | PFMinQ3 | int16 | PFMin_SF | Min power factor Q3 | | 16 | PFMinQ4 | int16 | PFMin_SF | Min power factor Q4 | | 17 | VArAct | enum16 | — | VAR action on mode change | | 18 | ClcTotVA | enum16 | — | Total VA calculation method | | 19 | MaxRmpRte | uint16 | MaxRmpRte_SF | Maximum ramp rate (%WGra) | | 20 | ECPNomHz | uint16 | ECPNomHz_SF | Nominal frequency (Hz) | | 21 | ConnPh | enum16 | — | Connected phase (single phase) |


Model 123 — Inverter Controls

Method: readInverterControls() / writeInverterControls()

Scale Factors

| Offset | Field | Type | Description | |--------|-------|------|-------------| | 21 | WMaxLimPct_SF | int16 | Power limit SF | | 22 | OutPFSet_SF | int16 | Power factor SF | | 23 | VArPct_SF | int16 | VAR percentage SF |

Connection Control

| Offset | Field | Type | R/W | Description | |--------|-------|------|-----|-------------| | 0 | Conn_WinTms | uint16 | R | Time window for connect/disconnect (sec) | | 1 | Conn_RvrtTms | uint16 | R | Timeout for connect/disconnect (sec) | | 2 | Conn | enum16 | W | Connect/disconnect (0=DISCONNECT, 1=CONNECT) |

Power Limit Control

| Offset | Field | Type | R/W | SF | Description | |--------|-------|------|-----|----|-------------| | 3 | WMaxLimPct | uint16 | W | WMaxLimPct_SF | Power limit (%WMax) | | 4 | WMaxLimPct_WinTms | uint16 | R | — | Time window (sec) | | 5 | WMaxLimPct_RvrtTms | uint16 | R | — | Timeout (sec) | | 6 | WMaxLimPct_RmpTms | uint16 | R | — | Ramp time (sec) | | 7 | WMaxLim_Ena | enum16 | W | — | Enable (0=DISABLED, 1=ENABLED) |

Power Factor Control

| Offset | Field | Type | R/W | SF | Description | |--------|-------|------|-----|----|-------------| | 8 | OutPFSet | int16 | W | OutPFSet_SF | Power factor setpoint | | 9 | OutPFSet_WinTms | uint16 | R | — | Time window (sec) | | 10 | OutPFSet_RvrtTms | uint16 | R | — | Timeout (sec) | | 11 | OutPFSet_RmpTms | uint16 | R | — | Ramp time (sec) | | 12 | OutPFSet_Ena | enum16 | W | — | Enable (0=DISABLED, 1=ENABLED) |

Reactive Power Control

| Offset | Field | Type | R/W | SF | Description | |--------|-------|------|-----|----|-------------| | 13 | VArWMaxPct | int16 | R | VArPct_SF | Reactive power at max W (%) | | 14 | VArMaxPct | int16 | R | VArPct_SF | Max reactive power (%) | | 15 | VArAvalPct | int16 | R | VArPct_SF | Available reactive power (%) | | 16 | VArPct_WinTms | uint16 | R | — | Time window (sec) | | 17 | VArPct_RvrtTms | uint16 | R | — | Timeout (sec) | | 18 | VArPct_RmpTms | uint16 | R | — | Ramp time (sec) | | 19 | VArPct_Mod | enum16 | R | — | Mode (0=NONE, 1=WMAX, 2=VARMAX) | | 20 | VArPct_Ena | enum16 | R | — | Enable (0=DISABLED, 1=ENABLED) |


Model 124 — Battery Basic Storage Controls

Method: readBatteryData() / writeBatteryControls()

Scale Factors

| Offset | Field | Type | Description | |--------|-------|------|-------------| | 18 | WChaMax_SF | int16 | Max charge power SF | | 19 | WChaDisChaGra_SF | int16 | Charge/discharge gradient SF | | 20 | VAChaMax_SF | int16 | Max charging VA SF | | 21 | MinRsvPct_SF | int16 | Min reserve percentage SF | | 22 | ChaState_SF | int16 | Charge state SF | | 23 | StorAval_SF | int16 | Available storage SF | | 24 | InBatV_SF | int16 | Battery voltage SF | | 25 | InOutWRte_SF | int16 | Charge/discharge rate SF |

Data Registers

| Offset | Field | Type | R/W | SF | Description | |--------|-------|------|-----|----|-------------| | 2 | WChaMax | uint16 | W | WChaMax_SF | Max charge power (W) | | 3 | WChaGra | uint16 | R | WChaDisChaGra_SF | Charge rate gradient (%/sec) | | 4 | WDisChaGra | uint16 | R | WChaDisChaGra_SF | Discharge rate gradient (%/sec) | | 5 | StorCtlMod | bitfield16 | W | — | Storage control mode (see enum) | | 6 | VAChaMax | uint16 | R | VAChaMax_SF | Max charging VA | | 7 | MinRsvPct | uint16 | W | MinRsvPct_SF | Min reserve (%) | | 8 | ChaState | uint16 | R | ChaState_SF | State of charge (%AhrRtg) | | 9 | StorAval | uint16 | R | StorAval_SF | Available storage (AH) | | 10 | InBatV | uint16 | R | InBatV_SF | Battery voltage (V) | | 11 | ChaSt | enum16 | R | — | Charge status (see enum) | | 12 | OutWRte | int16 | W | InOutWRte_SF | Discharge rate (%WDisChaMax) | | 13 | InWRte | int16 | W | InOutWRte_SF | Charge rate (%WChaMax) | | 14 | InOutWRteWinTms | uint16 | R | — | Rate change time window (sec) | | 15 | InOutWRteRvrtTms | uint16 | R | — | Rate change timeout (sec) | | 16 | InOutWRteRmpTms | uint16 | R | — | Rate change ramp time (sec) | | 17 | ChaGriSet | enum16 | W | — | Charge source (0=PV, 1=GRID) |


Model 160 — MPPT (Multiple Maximum Power Point Tracker)

Method: readAllMPPTData() / readMPPTData(moduleId)

Fixed Block (Scale Factors)

| Offset | Field | Type | Description | |--------|-------|------|-------------| | 2 | DCA_SF | int16 | DC current SF | | 3 | DCV_SF | int16 | DC voltage SF | | 4 | DCW_SF | int16 | DC power SF | | 5 | DCWH_SF | int16 | DC energy SF | | 8 | N | uint16 | Number of MPPT modules |

Repeating Module Block (20 registers each)

Module 1 starts at offset 10, Module 2 at offset 30, Module N at offset 10 + (N-1) × 20.

| Relative Offset | Field | Type | SF | Description | |-----------------|-------|------|----|-------------| | 0 | ID | uint16 | — | Module ID | | 1–8 | IDStr | string (8 regs) | — | Module string identifier | | 9 | DCA | uint16 | DCA_SF | DC current (A) | | 10 | DCV | uint16 | DCV_SF | DC voltage (V) | | 11 | DCW | uint16 | DCW_SF | DC power (W) | | 12–13 | DCWH | acc32 | DCWH_SF | Cumulative DC energy (Wh) | | 14–15 | Tms | uint32 | — | Timestamp (seconds since epoch) | | 16 | Tmp | int16 | fixed (−1) | Module temperature (°C) | | 17 | DCSt | enum16 | — | Operating state |


Models 201/203/204 — Meters

Method: readMeterData()

The SDK reads Model 203 (3-Phase Delta), 204 (3-Phase Wye), or 201 (Single-Phase) — trying in that order. All three share the same register offsets for the fields we read:

Scale Factors

| Offset | Field | Type | Description | |--------|-------|------|-------------| | 17 | Hz_SF | int16 | Frequency SF | | 22 | W_SF | int16 | Power SF | | 54 | TotWh_SF | int16 | Energy SF |

Data Registers

| Offset | Field | Type | SF | Description | |--------|-------|------|----|-------------| | 16 | Hz | uint16 | Hz_SF | Frequency (Hz) | | 18 | W | int16 | W_SF | Total real power (W) | | 38–39 | TotWhExp | acc32 | TotWh_SF | Total exported energy (Wh) | | 46–47 | TotWhImp | acc32 | TotWh_SF | Total imported energy (Wh) |


Model 802 — Battery Base

Method: readBatteryBaseData()

Scale Factors

| Offset | Field | Type | Description | |--------|-------|------|-------------| | 52 | AHRtg_SF | int16 | Amp-hour rating SF | | 53 | WHRtg_SF | int16 | Watt-hour rating SF | | 54 | WChaDisChaMax_SF | int16 | Charge/discharge max rate SF | | 55 | DisChaRte_SF | int16 | Self-discharge rate SF | | 56 | SoC_SF | int16 | State of charge SF | | 57 | DoD_SF | int16 | Depth of discharge SF | | 58 | SoH_SF | int16 | State of health SF | | 59 | V_SF | int16 | Voltage SF | | 60 | CellV_SF | int16 | Cell voltage SF | | 61 | A_SF | int16 | Current SF | | 62 | AMax_SF | int16 | Max current SF | | 63 | W_SF | int16 | Power SF |

Nameplate (Offsets 2–5)

| Offset | Field | Type | SF | Description | |--------|-------|------|----|-------------| | 2 | AHRtg | uint16 | AHRtg_SF | Nameplate charge capacity (AH) | | 3 | WHRtg | uint16 | WHRtg_SF | Nameplate energy capacity (WH) | | 4 | WChaRteMax | uint16 | WChaDisChaMax_SF | Max charge rate (W) | | 5 | WDisChaRteMax | uint16 | WChaDisChaMax_SF | Max discharge rate (W) |

State of Charge / Health (Offsets 6–13)

| Offset | Field | Type | SF | Description | |--------|-------|------|----|-------------| | 6 | DisChaRte | uint16 | DisChaRte_SF | Self-discharge rate (%) | | 7 | SoCMax | uint16 | SoC_SF | Max state of charge (%) | | 8 | SoCMin | uint16 | SoC_SF | Min state of charge (%) | | 9 | SoCRsvMax | uint16 | SoC_SF | Max reserve SOC (%) | | 10 | SoCRsvMin | uint16 | SoC_SF | Min reserve SOC (%) | | 11 | SoC | uint16 | SoC_SF | Current state of charge (%) | | 12 | DoD | uint16 | DoD_SF | Depth of discharge (%) | | 13 | SoH | uint16 | SoH_SF | State of health (%) |

Status (Offsets 14–22)

| Offset | Field | Type | Description | |--------|-------|------|-------------| | 14–15 | NCyc | uint32 | Cycle count | | 16 | ChaSt | enum16 | Charge status (see enum) | | 17 | LocRemCtl | enum16 | Local/remote control mode | | 21 | Typ | enum16 | Battery type (see enum) | | 22 | State | enum16 | Battery bank state (see enum) |

Events (Offsets 26–33)

| Offset | Field | Type | Description | |--------|-------|------|-------------| | 26–27 | Evt1 | bitfield32 | Event bitfield 1 | | 28–29 | Evt2 | bitfield32 | Event bitfield 2 | | 30–31 | EvtVnd1 | bitfield32 | Vendor event bitfield 1 | | 32–33 | EvtVnd2 | bitfield32 | Vendor event bitfield 2 |

Voltage (Offsets 34–43)

| Offset | Field | Type | SF | Description | |--------|-------|------|----|-------------| | 34 | V | uint16 | V_SF | Battery voltage (V) | | 35 | VMax | uint16 | V_SF | Max battery voltage (V) | | 36 | VMin | uint16 | V_SF | Min battery voltage (V) | | 37 | CellVMax | uint16 | CellV_SF | Max cell voltage (V) | | 38 | CellVMaxStr | uint16 | — | String containing max cell | | 39 | CellVMaxMod | uint16 | — | Module containing max cell | | 40 | CellVMin | uint16 | CellV_SF | Min cell voltage (V) | | 41 | CellVMinStr | uint16 | — | String containing min cell | | 42 | CellVMinMod | uint16 | — | Module containing min cell | | 43 | CellVAvg | uint16 | CellV_SF | Average cell voltage (V) |

Current (Offsets 44–46)

| Offset | Field | Type | SF | Description | |--------|-------|------|----|-------------| | 44 | A | int16 | A_SF | Battery current (A) | | 45 | AChaMax | uint16 | AMax_SF | Max charge current (A) | | 46 | ADisChaMax | uint16 | AMax_SF | Max discharge current (A) |

Power / Control (Offsets 47–51)

| Offset | Field | Type | SF | Description | |--------|-------|------|----|-------------| | 47 | W | int16 | W_SF | Battery power (W, +charge/−discharge) | | 48 | ReqInvState | enum16 | — | Requested inverter state | | 49 | ReqW | int16 | W_SF | Requested power (W) | | 50 | SetOp | enum16 | — | Set operation | | 51 | SetInvState | enum16 | — | Set inverter state |


Writable Registers

Model 123 — Inverter Controls (writeInverterControls)

| Register Offset | Field | Scale Factor Offset | Description | |----------------|-------|---------------------|-------------| | 2 | Conn | — | Connect/disconnect | | 3 | WMaxLimPct | 21 (WMaxLimPct_SF) | Power limit percentage | | 7 | WMaxLim_Ena | — | Throttle enable/disable | | 8 | OutPFSet | 22 (OutPFSet_SF) | Power factor setpoint | | 12 | OutPFSet_Ena | — | PF control enable/disable |

Model 124 — Battery Controls (writeBatteryControls)

| Register Offset | Field | Scale Factor Offset | Description | |----------------|-------|---------------------|-------------| | 2 | WChaMax | 18 (WChaMax_SF) | Max charge power (W) | | 5 | StorCtlMod | — | Storage control mode bits | | 7 | MinRsvPct | 21 (MinRsvPct_SF) | Min reserve percentage | | 12 | OutWRte | 25 (InOutWRte_SF) | Discharge rate (%) | | 13 | InWRte | 25 (InOutWRte_SF) | Charge rate (%) | | 17 | ChaGriSet | — | Charge source (PV/Grid) |

Helper: setFeedInLimit(limitW)

Reads WMax from Model 121, computes (limitW / WMax) × 100, then writes WMaxLimPct + enables WMaxLim_Ena via Model 123. Pass null to disable the limit.

Helper: setStorageMode(mode) / enableGridCharging(enable)

Convenience wrappers around writeBatteryControls() for common battery operations.


Enum Reference

SunspecBatteryChargeState (Model 124 offset 11 / Model 802 offset 16)

| Value | Name | Description | |-------|------|-------------| | 1 | OFF | Battery off | | 2 | EMPTY | Battery empty | | 3 | DISCHARGING | Discharging | | 4 | CHARGING | Charging | | 5 | FULL | Fully charged | | 6 | HOLDING | Holding charge | | 7 | TESTING | Under test |

SunspecStorageControlMode (Model 124 offset 5 — bitfield)

| Bit | Value | Name | Description | |-----|-------|------|-------------| | 0 | 0x0001 | CHARGE | Enable charging | | 1 | 0x0002 | DISCHARGE | Enable discharging |

Derived modes via setStorageMode():

| Mode | Bits | Description | |------|------|-------------| | CHARGE | 0x01 | Charge only | | DISCHARGE | 0x02 | Discharge only | | HOLDING | 0x00 | Neither charge nor discharge | | AUTO | 0x03 | Both charge and discharge |

SunspecChargeSource (Model 124 offset 17)

| Value | Name | Description | |-------|------|-------------| | 0 | PV | Charge from PV only | | 1 | GRID | Charge from grid |

SunspecEnableControl (Model 123 — various enable fields)

| Value | Name | |-------|------| | 0 | DISABLED | | 1 | ENABLED |

SunspecBatteryType (Model 802 offset 21)

| Value | Name | |-------|------| | 0 | NOT_APPLICABLE_UNKNOWN | | 1 | LEAD_ACID | | 2 | NICKEL_METAL_HYDRIDE | | 3 | NICKEL_CADMIUM | | 4 | LITHIUM_ION | | 5 | CARBON_ZINC | | 6 | ZINC_CHLORIDE | | 7 | ALKALINE | | 8 | RECHARGEABLE_ALKALINE | | 9 | SODIUM_SULFUR | | 10 | FLOW | | 11 | SUPER_CAPACITOR | | 99 | OTHER |

SunspecBatteryBankState (Model 802 offset 22)

| Value | Name | |-------|------| | 1 | DISCONNECTED | | 2 | INITIALIZING | | 3 | CONNECTED | | 4 | STANDBY | | 5 | SOC_PROTECTION | | 6 | SUSPENDING | | 99 | FAULT |