Skip to main content

Specification v0.4

info

This is the rendered normative specification, synced from moddef/spec. The Markdown source is the source of truth.

Status: Draft
Version: 0.4
Primary file extensions: .moddef, .moddef.json, .moddef.yaml
Canonical schema: Protobuf
Primary domain: Modbus register map and semantic device model description

v0.4 (2026-06-04) adds the encoding constructs that v0.3 turned out to be missing once it was checked against 136 real device Modbus specifications (energy meters, solar inverters, BESS, EV chargers, HVAC heat pumps). The new constructs are register-referenced scale factors, unavailable/sentinel values, discovered (SunSpec model-chain) addressing, conditional point presence, register-field structs, flag sets, date/time values, repeating arrays with a stride, a few extra storage widths, and distinct write encodings. See §37 Revision History for the full list and the reasons behind it (research/MODDEF-GAP-ANALYSIS.md).

1. Overview

ModDef is a portable file format for describing Modbus devices, their registers, register encodings, data types, semantic meanings, and generated client APIs.

A ModDef file describes both:

  1. The physical Modbus mapping, where a value is located and how it is encoded.
  2. The semantic meaning, what the value represents, such as active power import, frequency, state of charge, or voltage measured between L1 and neutral.

This separation allows tooling to generate typed clients, validate register maps, expose standard methods, and reuse shared definitions across manufacturers and standards.

2. Goals

ModDef is designed to:

  • Provide a machine-readable replacement for vendor PDF and spreadsheet register maps.
  • Support binary, JSON, and YAML encodings with equivalent semantics.
  • Allow strong validation through a linter and compiler.
  • Support code generation for multiple programming languages.
  • Support reusable standard libraries, including SunSpec mappings and ModDef core measurands.
  • Make semantic data access possible through generated methods such as get_active_power_import() or get_voltage(phase="L1-N").
  • Support low-power and embedded use cases via a compact binary representation.
  • Preserve enough detail to avoid ambiguity around endianness, scaling, units, access permissions, and register address interpretation.

3. Non-goals

ModDef does not:

  • Define the Modbus protocol itself.
  • Define how devices are discovered on the network.
  • Require devices to expose self-description over Modbus.
  • Replace SunSpec, OCPP, IEC, or other domain standards.
  • Guarantee that two vendors use the same physical register addresses.
  • Require end users to know or use Protobuf directly.
  • Describe non-Modbus transports or control planes. ModDef is a Modbus description format. Devices whose only machine interface is OCPP, REST, CAN, DNP3, eBUS/EMS, BACnet objects, or a proprietary serial frame are out of scope and cannot be fully modeled by ModDef. (Validation against 136 real device documents found ~28 such devices; see §37.) A future transport-neutral sibling format may address them; v1 does not.

4. File Types

ModDef has three equivalent representations.

4.1 Binary Representation

Extension:

.moddef

The .moddef file is the binary representation of a ModDef document.

Recommended encoding:

  • Protobuf binary encoding of ModDefDocument.
  • Optional wrapper header may be introduced later for magic bytes, schema version, compression, and signatures.
  • The first stable release should avoid compression by default to simplify embedded implementations.

4.2 JSON Representation

Extension:

.moddef.json

The .moddef.json file is the JSON representation of the same document.

It is intended for:

  • Programmatic tooling.
  • Web services.
  • Git diffs where YAML is not preferred.
  • Schema validation.

4.3 YAML Representation

Extension:

.moddef.yaml

The .moddef.yaml file is the human-editable representation.

It is intended for:

  • Handwritten device profiles.
  • Documentation examples.
  • Review in pull requests.
  • Manufacturer authored files.

YAML comments are allowed but must not carry semantic meaning.

4.4 Equivalence Requirement

The following conversions must be lossless:

.moddef.yaml -> .moddef.json -> .moddef
.moddef -> .moddef.json -> .moddef.yaml
.moddef.json -> .moddef -> .moddef.yaml

Canonical tooling must preserve all semantic information.

5. Terminology

5.1 Document

A complete ModDef file.

5.2 Device Profile

A definition for a specific device, product family, or reusable device class.

5.3 Point

A user-visible data point exposed by a device.

Examples:

  • Total active power import
  • Frequency
  • L1-N voltage
  • Battery state of charge
  • Device status
  • Alarm bit field

5.4 Mapping

The physical Modbus location and encoding for a point.

5.5 Register

A 16-bit Modbus register in either the input register or holding register address space.

5.6 Word

A 16-bit quantity. In Modbus register spaces, one register equals one word.

5.7 Measurand

The semantic meaning of a point.

Examples:

  • active_power
  • energy_active_register
  • voltage
  • frequency
  • state_of_charge

A measurand may be qualified by direction, phase, reference, location, aggregation, and context.

6. Document Structure

A ModDef document contains:

  • Metadata
  • Imports
  • Type definitions
  • Measurand definitions
  • Device profiles
  • Register blocks
  • Points
  • Optional variants
  • Optional standard library references

Conceptual structure:

doc_id: example.energy-meter
name: Example Energy Meter
version: 0.1.0

imports:
- uri: moddef:stdlib:measurands:1.0.0
alias: meas

enums:
- type_id: device_status
values:
- value: 0
name: ok
- value: 1
name: warning
- value: 2
name: fault

devices:
- device_id: example-meter
vendor: Example Corp
model: EM-1000
blocks:
- block_id: electrical-measurements
space: HOLDING_REGISTER
start_offset: 0
length_words: 100
points:
- point_id: frequency
name: Grid Frequency
measurand:
measurand_id: frequency
mapping:
space: HOLDING_REGISTER
offset: 10
length_words: 1
storage_type: U16
value_type:
primitive: DECIMAL
transform:
scale:
numerator: 1
denominator: 100
unit: Hz
access: READ_ONLY

7. Address Spaces

ModDef explicitly supports the four Modbus address spaces.

Address space Modbus concept Typical access


COIL 1-bit read/write values read/write DISCRETE_INPUT 1-bit read-only values read-only INPUT_REGISTER 16-bit read-only registers read-only HOLDING_REGISTER 16-bit read/write registers read/write

7.1 Addressing Rule

ModDef stores Modbus addresses as 0-based offsets within each address space.

This avoids ambiguity between:

  • Protocol-level offsets.
  • Vendor documentation addresses.
  • Human notations such as 40001.

Example:

Vendor notation ModDef space ModDef offset


40001 HOLDING_REGISTER 0 40083 HOLDING_REGISTER 82 30001 INPUT_REGISTER 0

Tools may optionally accept human notation, but the canonical document must use 0-based offsets.

7.2 Function Codes

The ModDef address space implies compatible function codes.

Space Read functions Write functions


COIL 01 05, 15 DISCRETE_INPUT 02 none INPUT_REGISTER 04 none HOLDING_REGISTER 03 06, 16

A point may optionally constrain supported function codes if a device has non-standard behavior.

7.3 Dynamic and Discovered Addressing (v0.4)

Some devices do not place data at fixed offsets. The most important case is SunSpec, where a device exposes a chain of models discovered at runtime: a well-known anchor register holds the marker SunS (0x53756E53), followed by repeating (model_id, length) headers that must be walked to learn where each model (and therefore each point) actually lives. Addresses also shift with device composition (e.g. Fronius moves blocks depending on float-vs-integer model selection and whether a meter is present).

A register block may declare a discovery anchor so that the offsets of its points are resolved relative to a model located at run time, rather than being absolute:

blocks:
- block_id: sunspec-inverter
space: HOLDING_REGISTER
discovery:
kind: SUNSPEC # walk SunS marker + (model_id,length) chain
anchor_candidates: [0, 40000, 50000]
model_id: 103 # resolve this block to SunSpec model 103
points:
- point_id: ac_power
mapping:
model_relative_offset: 14 # offset within the resolved model
storage_type: S16
value_type: { primitive: DECIMAL }

Rules:

  • When discovery.kind is SUNSPEC, point offsets are interpreted as model_relative_offset within the resolved model; the absolute address is computed by the codec after the model chain is walked.
  • model_relative_offset is measured from the model's ID register: offset 0 is the model id, offset 1 the model length, and data begins at offset 2. This matches the canonical SunSpec model tables (e.g. model 103 W at offset 14, as in the example above).
  • anchor_candidates lists the register addresses to probe for the SunS marker (commonly 0, 40000, or 50000).
  • A block with no discovery uses absolute offsets as before (default).

See also §17.1 (conditional point presence), which handles points that exist only in some device compositions.

8. Data Types

ModDef separates logical type from physical storage type.

This is necessary because a value may be logically a decimal quantity but physically stored as an integer with scaling.

8.1 Logical Types

Logical types describe what the value means to users and generated clients.

Supported logical types:

  • BOOL
  • INT32
  • UINT32
  • INT64
  • UINT64
  • FLOAT32
  • FLOAT64
  • DECIMAL
  • STRING
  • BYTES
  • ENUM
  • STRUCT
  • ARRAY
  • DATETIME (v0.4): a calendar date/time or duration; see §8.5.
  • FLAGS (v0.4): a set of independently-true boolean flags; see §13.2.

8.2 Storage Types

Storage types describe how values are physically encoded in Modbus.

Supported storage types:

  • BIT
  • U16
  • S16
  • U32
  • S32
  • U64
  • S64
  • IEEE754_F32
  • IEEE754_F64
  • STRING_ASCII
  • STRING_UTF8
  • BYTES_RAW
  • BCD
  • FIXED_POINT
  • COMPOSED
  • U24, U48 (v0.4): 24-bit and 48-bit unsigned integers.
  • S48 (v0.4): 48-bit signed (two's complement) integer.

These odd widths appear in real maps (e.g. 48-bit energy counters). Signed storage types are two's complement by default; sign_magnitude: true may be set on a mapping where a device uses sign-magnitude instead.

8.3 Required Typing Rule

Each point must define:

  • value_type
  • storage_type

This prevents ambiguity.

Example:

storage_type: U16
value_type:
primitive: DECIMAL
transform:
scale:
numerator: 1
denominator: 10

This means the device stores an unsigned 16-bit integer, but the exposed value is decimal.

8.4 Unavailable / Sentinel Values (v0.4)

Almost every real register map reserves one or more raw values that mean "not implemented", "no data", or "over-range" rather than a real reading. Common conventions:

  • SunSpec: 0x8000 for int16, 0x80000000 for int32, 0xFFFF/ 0xFFFFFFFF for unsigned "not accumulated", and a NaN for floats.
  • ABB: 0x7FFF… (max positive); Carlo Gavazzi: 0x7FFF → display EEEE.

ModDef v0.3 had no way to declare these, so a reader could not distinguish a real value from "unavailable". v0.4 adds an optional, per-point sentinel set:

storage_type: S16
value_type: { primitive: DECIMAL }
na_values:
- raw: 0x8000 # decoded result is "unavailable", not a number

Rules:

  • na_values lists raw register patterns (after width/endianness decode, before scaling) that decode to a typed "unavailable" result.
  • A default sentinel set MAY be supplied by a storage_type profile in a standard library (e.g. the SunSpec stdlib package) and inherited by points.
  • Generated clients expose unavailable readings as a typed absence (e.g. None/nil/Optional.empty), never as the raw sentinel.

8.5 Date and Time Values (v0.4)

A DATETIME logical type carries a calendar instant or duration. Devices encode these several ways; the encoding is declared explicitly:

value_type: { primitive: DATETIME }
datetime:
encoding: EPOCH_S # seconds since 1970-01-01T00:00:00Z
width: U32 # or U64

Supported encoding values:

  • EPOCH_S / EPOCH_MS: unsigned seconds/milliseconds since the Unix epoch, stored in a U32/U64.
  • PACKED_BCD_DATETIME: BCD-packed YYYYMMDDhhmmss across registers.
  • SPLIT_FIELDS: separate year/month/day/hour/minute/second sub-fields, described with a register-field struct (§13.1).

DATETIME values are not measurands; they are typically timestamps on log entries, RTC registers, or energy-counter snapshots.

9. Endianness and Word Order

Modbus registers are 16-bit words. Multi-word values must define how words and bytes are ordered.

9.1 Byte Order

Byte order describes byte order within each 16-bit word.

Values:

  • BIG_ENDIAN
  • LITTLE_ENDIAN

9.2 Word Order

Word order describes ordering of 16-bit words in multi-word values.

Values:

  • WORD_BIG_ENDIAN
  • WORD_LITTLE_ENDIAN

9.3 Defaults and Warnings

For single 16-bit register values, byte order is usually not meaningful beyond Modbus wire format.

For multi-word values, tools must warn if either byte order or word order is omitted.

Recommended linter rule:

MDW001: Multi-word value does not explicitly specify byte_order and word_order.

10. Scaling, Offset, and Units

10.1 Rational Scaling

Scale and offset must be represented as rational values, not floating point values.

scale:
numerator: 1
denominator: 100

This avoids cross-language differences when generating code.

10.2 Transform Pipeline

The value transform pipeline is:

  1. Read raw register or bit data.
  2. Decode using storage type, width, sign, byte order, and word order.
  3. Apply scale.
  4. Apply offset.
  5. Apply clamp, if defined.
  6. Apply enum mapping, if defined.
  7. Return logical value.

Formula:

value = raw * scale + offset

10.3 Units

Units should use stable textual identifiers.

Recommended convention:

  • Use UCUM-compatible units where possible.
  • Allow practical electrical unit strings such as W, kW, Wh, kWh, V, A, Hz, %, degC.
  • Linter should warn on unknown unit strings when a known unit exists.

10.4 Dynamic (Register-Referenced) Scaling (v0.4)

The static rational scale of §10.1 cannot express scaling whose factor is read from another register at run time. This is mandatory for SunSpec, since every SunSpec measurement has a companion scale-factor (SF) register and the real value is raw · 10^SF. Several non-SunSpec meters do the same with a decimal-point register or a CT/VT transformer-factor register (Eaton IQ, Janitza short-format, Carrier).

v0.4 lets a transform reference another point for its scale:

# SunSpec-style: value = raw * 10^(value of the referenced SF point)
transform:
scale_ref:
point_id: ac_power_sf # the SF register, read at the same time
mode: POW10 # value = raw * 10^ref
# Generic register-referenced multiplier: value = raw * (value of ref) / div
transform:
scale_ref:
point_id: ct_ratio
mode: MULTIPLY
denominator: 1

Rules:

  • Exactly one of scale (static rational, §10.1) or scale_ref may be set.
  • mode: POW10value = raw · 10^ref (the SunSpec convention; ref is a signed integer, often int16). mode: MULTIPLYvalue = raw · ref / denominator.
  • The referenced point must resolve within the same device profile and is read in the same transaction window; the linter errors if it does not resolve (MDE407, reserved).
  • offset (§10.1) still applies after the dynamic scale.

This is the single most important v0.4 addition: it unblocks essentially every SunSpec device (Fronius, SolarEdge, SMA, Kostal, Solplanet, and parts of Sungrow).

10.5 Value, Scale, or Unit Selected by Another Register (v0.4)

Some devices change the meaning, scale decade, or unit of a register based on the value of another register: an energy "format" register selecting units/kilo/mega, a tariff selector gating which accumulator a register represents, or an OBIS-coded quantity selector. A point may declare a selector_ref describing this indirection:

selector_ref:
point_id: energy_format # value selects the interpretation below
cases:
0: { scale: { numerator: 1, denominator: 1 } } # units
1: { scale: { numerator: 1000, denominator: 1 } } # kilo
2: { scale: { numerator: 1000000, denominator: 1 } }# mega

The codec evaluates the referenced point, then applies the matching case. Lower frequency than §10.4 but required for a handful of meters.

11. Access Modes and Write Semantics

11.1 Access Modes

Supported access modes:

  • READ_ONLY
  • WRITE_ONLY
  • READ_WRITE
  • COMMAND

11.2 Command Points

Command points represent writes that trigger actions.

Examples:

  • Reset energy counter.
  • Clear alarm.
  • Start charging.
  • Stop inverter.
  • Reboot device.

Command point fields may include:

  • write_behavior
  • confirmation_point
  • allowed_values
  • requires_enable_point

11.3 Write Behaviors

Supported write behaviors:

  • DIRECT
  • MOMENTARY
  • LATCH
  • MASKED
  • COMMAND_TRIGGER

11.4 Write Constraints

A writable point may define:

  • Minimum value
  • Maximum value
  • Step
  • Allowed values
  • Required enable condition
  • Cooldown time
  • Safety note

11.5 Distinct Write Encoding (v0.4)

A few devices encode a value differently on write than on read. For example, Carlo Gavazzi accepts a command with a "magic" high byte that does not appear in the read-back. A point may define a write_encoding block that overrides the storage/transform used for writes:

access: READ_WRITE
storage_type: U16
transform: { scale: { numerator: 1, denominator: 10 } } # read path
write_encoding:
storage_type: U16
transform: { scale: { numerator: 1, denominator: 1 } } # write path
prefix_high_byte: 0x80 # device "magic"

When write_encoding is absent, the read encoding is used for writes (the default and the common case).

11.6 Packed Command Arguments (v0.4)

Some command registers pack several operands into one written word (a sub-command id plus an argument, or HIWORD/LOWWORD operands). These are described as a write-side register-field struct (§13.1) on the command point, so each argument has its own bit range, type, and allowed values.

12. Enums

Enums are independent reusable type definitions.

An enum is not defined inside a register or point. A point references an enum type.

Example:

enums:
- type_id: operating_state
name: Operating State
values:
- value: 0
name: off
- value: 1
name: standby
- value: 2
name: running
- value: 3
name: fault

Point reference:

point_id: operating_state
storage_type: U16
value_type:
enum_ref:
type_id: operating_state

Validation rules:

  • Enum names must be unique within an enum.
  • Enum numeric values must be unique unless aliases are explicitly allowed.
  • Referenced enum types must exist after import resolution.
  • Linter must report unused enum definitions as optional warnings.

13. Bit Fields

A point may represent:

  • A single bit.
  • A range of bits.
  • A named set of bit fields inside a register.

Example:

point_id: alarm_flags
name: Alarm Flags
mapping:
space: HOLDING_REGISTER
offset: 20
length_words: 1
bit_fields:
- field_id: over_voltage
bit_offset: 0
bit_length: 1
- field_id: under_voltage
bit_offset: 1
bit_length: 1

Multi-bit fields may reference an enum.

13.1 Register Field Extraction / Sub-fields (v0.4)

A bare bit_field (above) extracts a bit range as a boolean or small enum. Real maps frequently pack several independent, fully-typed sub-values into one register or register pair: HIWORD/LOWWORD halves, a byte-tuple firmware version (major.minor.revision), a packed IPv4 address, or a packed-BCD blob holding several sub-values (Eastron demand interval / scroll time / backlight).

v0.4 generalizes bit_fields so each sub-field carries its own type, scale, and unit, i.e. a struct laid over a register window:

point_id: firmware_version
mapping: { space: HOLDING_REGISTER, offset: 8, length_words: 2 }
fields:
- field_id: major
bit_offset: 24
bit_length: 8
value_type: { primitive: UINT32 }
- field_id: minor
bit_offset: 16
bit_length: 8
value_type: { primitive: UINT32 }
- field_id: revision
bit_offset: 0
bit_length: 16
value_type: { primitive: UINT32 }

Each field may carry its own value_type, transform (including scale), unit, enum_ref, and storage_type (e.g. BCD for a packed-BCD sub-value). bit_offset is counted across the whole mapped window (LSB = 0). The legacy bit_fields form remains valid and is the boolean/enum special case.

13.2 Flag Sets (FLAGS) (v0.4)

Status/alarm registers often expose many independent boolean flags that may be true simultaneously (a bitmask), not a single enumerated state. Modeling each as a separate bit_field is verbose; the FLAGS value type names them as a set:

point_id: alarm_flags
storage_type: U32
value_type:
flags:
bits:
0: over_voltage
1: under_voltage
2: over_temperature
7: door_open

A decoded FLAGS value is the set of names whose bits are set. Unlisted bits are preserved as raw for round-tripping.

14. Composed Values

Some devices represent a numeric value as multiple separate registers.

Example:

  • Mantissa register.
  • Exponent register.

This can represent scientific notation:

value = mantissa * base^exponent

Example:

point_id: active_power
storage_type: COMPOSED
composed:
kind: MANTISSA_EXPONENT
base: 10
mantissa:
space: HOLDING_REGISTER
offset: 100
storage_type: S16
exponent:
space: HOLDING_REGISTER
offset: 101
storage_type: S16

Validation rules:

  • Both mantissa and exponent mappings must be valid.
  • Base must be non-zero.
  • Generated clients must expose the computed value, not the raw pair, unless raw access is requested.

14.1 Repeating Blocks and Arrays (v0.4)

Many devices repeat a group of registers a fixed or runtime-determined number of times with a constant stride: multi-string PV inputs, per-phase groups, per-cell/per-module battery data, and SunSpec repeating blocks. v0.4 adds stride_words and an optional count_ref to the array/struct mapping:

point_id: cell_voltages
value_type:
array:
element_type: { primitive: DECIMAL }
# length: 16 # fixed count, OR:
count_ref: { point_id: cell_count } # count read from a register
mapping:
space: INPUT_REGISTER
offset: 200
stride_words: 1 # each element starts 1 word after the previous
element_transform:
scale: { numerator: 1, denominator: 1000 }

For arrays of structs, element_type references a struct whose fields use window-relative offsets (§13.1) and stride_words is the size of one element. count_ref resolves a register holding the live element count; when absent, length is fixed.

15. Strings and Raw Bytes

String fields must define:

  • Encoding
  • Register length
  • Padding behavior
  • Termination behavior

Example:

storage_type: STRING_ASCII
length_words: 8
string_encoding:
charset: ASCII
padding: NULL
termination: NULL_TERMINATED

Recommended padding modes:

  • NULL
  • SPACE
  • NONE

Recommended termination modes:

  • FIXED_LENGTH
  • NULL_TERMINATED

16. Device Profiles

A device profile describes one device model, family, or reusable abstract device.

Fields:

  • device_id
  • vendor
  • model
  • family
  • description
  • firmware_versions
  • supported_transports
  • default_unit_id
  • blocks
  • variants

Example:

device_id: example-inverter
vendor: Example Corp
model: INV-5000
supported_transports:
- MODBUS_RTU
- MODBUS_TCP

17. Register Blocks

Register blocks group related points.

A block may correspond to:

  • A contiguous Modbus address range.
  • A functional group.
  • A standard model block, such as a SunSpec model.
  • A vendor-specific extension.

Fields:

  • block_id
  • name
  • space
  • start_offset
  • length_words
  • points

Validation rules:

  • Points in a block should fall within the block range.
  • Overlaps are errors unless explicitly allowed.
  • Blocks may overlap only if declared as overlays.

17.1 Conditional Point Presence (v0.4)

A point may exist only in some firmware versions, options, or device compositions (an optional meter, a battery module, a model present only when a feature register is set). Rather than forcing a separate variant for every combination, a point or block may declare an available_if condition:

- point_id: battery_soc
available_if:
point_id: battery_present # gating register/point
equals: 1
mapping: { space: HOLDING_REGISTER, offset: 520, length_words: 1 }
storage_type: U16
value_type: { primitive: DECIMAL }

A reader skips an unavailable point instead of reading garbage. available_if complements §7.3 discovery (which handles whole models appearing/moving) and §18 variants (which handle named SKUs).

18. Variants

Variants represent firmware, hardware, regional, or option-specific differences.

Example use cases:

  • Same model with different firmware.
  • Optional energy meter module.
  • Single phase and three phase versions.
  • Solar inverter with optional battery module.

A variant may:

  • Add points.
  • Remove points.
  • Override points.
  • Override metadata.

Example:

variants:
- variant_id: firmware-2
name: Firmware 2.x
base_device_id: example-meter
removals:
- deprecated_alarm_register
additions:
- point_id: state_of_charge
...

19. Imports and Packages

Imports allow reuse of external definitions.

19.1 Import URI Types

Supported import URI styles:

./relative-file.moddef.yaml
../common/enums.moddef.yaml
moddef:stdlib:core:1.0.0
moddef:stdlib:measurands:1.0.0
moddef:stdlib:sunspec:1.0.0

19.2 Import Aliases

An import may define an alias.

imports:
- uri: moddef:stdlib:measurands:1.0.0
alias: meas

Qualified references may use the alias:

measurand_id: meas:active_power

19.3 Conflict Rules

  • Local definitions override nothing implicitly.
  • Conflicting imported identifiers are errors unless namespaced by alias.
  • Re-exporting imports must be explicit.

19.4 Package Manifest

A ModDef package should include:

package_id: moddef:stdlib:measurands
version: 1.0.0
files:
- measurands.moddef.yaml
dependencies: []
checksum_algorithm: sha256

20. Standard Libraries

Recommended official packages:

20.1 Core Library

moddef:stdlib:core:<version>

Contains:

  • Common primitive enums.
  • Common status enums.
  • Standard write behavior definitions.
  • Common transform helpers.

20.2 Measurands Library

moddef:stdlib:measurands:<version>

Contains:

  • Core ModDef measurand definitions.
  • Electrical quantity taxonomy.
  • Phase and reference definitions.
  • Default unit compatibility rules.

20.3 SunSpec Library

moddef:stdlib:sunspec:<version>

Contains:

  • SunSpec model mappings represented in ModDef.
  • Versioned packages matching source model revisions where possible.
  • Measurand annotations for common SunSpec fields.

20.4 Vendor Libraries

Vendors may publish packages such as:

vendor:acme:meters:1.2.0
vendor:acme:inverters:2026.04

21. Core Measurands System

The Core Measurands System defines semantic identifiers that can be referenced by points.

The goal is to allow generated clients to expose high-level APIs independent of vendor register addresses.

Example:

device.get_active_power(direction="import")
device.get_voltage(phase="L1-N")
device.get_frequency()

If a device does not support a measurand, the client must return or raise a well-defined MeasurandNotSupported error.

22. Measurand Model

A measurand is composed of:

  • Base quantity
  • Optional direction
  • Optional phase/reference
  • Optional aggregation
  • Optional location
  • Optional context
  • Optional accumulation kind
  • Canonical unit
  • Expected value type

22.1 Base Quantity

Examples:

  • active_power
  • reactive_power
  • apparent_power
  • energy_active
  • energy_reactive
  • energy_apparent
  • voltage
  • current
  • frequency
  • power_factor
  • temperature
  • state_of_charge
  • state_of_health
  • device_status
  • alarm_status

22.2 Direction

Direction is used for quantities where flow direction matters.

Recommended values:

  • NONE
  • IMPORT
  • EXPORT
  • NET
  • CHARGE
  • DISCHARGE
  • FORWARD
  • REVERSE

Examples:

base_quantity: active_power
direction: IMPORT
base_quantity: energy_active
direction: EXPORT
accumulation: REGISTER

22.3 Phase and Reference

Phase and reference are modeled using a phase reference enum inspired by OCPP phase values.

Do not encode every phase combination as a separate measurand name. Instead, define the base measurand and qualify it with phase_ref.

Recommended PhaseRef values:

Value Meaning


NONE Not phase-specific L1 Phase L1 L2 Phase L2 L3 Phase L3 N Neutral L1_N L1 measured against neutral L2_N L2 measured against neutral L3_N L3 measured against neutral L1_L2 Line-to-line measurement from L1 to L2 L2_L3 Line-to-line measurement from L2 to L3 L3_L1 Line-to-line measurement from L3 to L1

Textual wire values may be rendered in OCPP-compatible form:

ModDef enum Text value


L1_N L1-N L2_N L2-N L3_N L3-N L1_L2 L1-L2 L2_L3 L2-L3 L3_L1 L3-L1

Examples:

Voltage between L1 and neutral:

measurand:
base_quantity: voltage
phase_ref: L1_N

Voltage between L1 and L2:

measurand:
base_quantity: voltage
phase_ref: L1_L2

Current on L1:

measurand:
base_quantity: current
direction: IMPORT
phase_ref: L1

Total active power import:

measurand:
base_quantity: active_power
direction: IMPORT
aggregation: TOTAL

22.4 Aggregation

Aggregation describes whether a measurement is per phase, total, average, minimum, maximum, or otherwise aggregated.

Recommended values:

  • NONE
  • TOTAL
  • AVERAGE
  • MINIMUM
  • MAXIMUM
  • INSTANTANEOUS

Examples:

base_quantity: active_power
direction: IMPORT
aggregation: TOTAL
base_quantity: voltage
phase_ref: L1_N
aggregation: INSTANTANEOUS

22.5 Location

Location describes where the measurement is taken.

Recommended values:

  • UNSPECIFIED
  • GRID
  • LOAD
  • INLET
  • OUTLET
  • BATTERY
  • PV
  • INVERTER
  • EV
  • CABLE
  • BODY
  • INTERNAL
  • EXTERNAL

This is useful for devices with multiple measurement locations.

Examples:

base_quantity: temperature
location: INTERNAL
base_quantity: energy_active
direction: IMPORT
location: GRID
accumulation: REGISTER

22.6 Accumulation

Accumulation describes whether a value is instantaneous, a cumulative register, or an interval value.

Recommended values:

  • INSTANTANEOUS
  • REGISTER
  • INTERVAL
  • LIFETIME
  • SESSION
  • DAILY
  • MONTHLY

Example:

base_quantity: energy_active
direction: IMPORT
accumulation: REGISTER

22.7 Context

Context describes why or when a value is sampled.

Recommended values:

  • UNSPECIFIED
  • SAMPLE_PERIODIC
  • SAMPLE_CLOCK
  • TRANSACTION_BEGIN
  • TRANSACTION_END
  • INTERRUPTION_BEGIN
  • INTERRUPTION_END
  • TRIGGERED
  • MANUAL

Context is optional and is primarily useful for telemetry streams, not static register maps.

23. Core Measurand Catalog

The core catalog should be extensible. It should not try to include every possible vendor-specific concept in v1.0.

23.1 Electrical Power


Base quantity Direction Default unit Notes


active_power import, export, W Instantaneous net real power

reactive_power import, export, var Instantaneous net reactive power

apparent_power none, import, VA Instantaneous export apparent power

power_factor none 1 Ratio, dimensionless

Phase-qualified examples:

  • active_power, direction IMPORT, phase_ref L1
  • active_power, direction IMPORT, phase_ref L2
  • active_power, direction IMPORT, phase_ref L3
  • active_power, direction IMPORT, aggregation TOTAL

23.2 Electrical Energy

Base quantity Direction Accumulation Default unit


energy_active import, export, net register Wh energy_reactive import, export, net register varh energy_apparent import, export register VAh

OCPP-style names can be mapped as aliases:


OCPP-style name ModDef semantic tuple


Energy.Active.Import.Register base energy_active, direction IMPORT, accumulation REGISTER

Energy.Active.Export.Register base energy_active, direction EXPORT, accumulation REGISTER

Energy.Reactive.Import.Register base energy_reactive, direction IMPORT, accumulation REGISTER

Energy.Reactive.Export.Register base energy_reactive, direction EXPORT, accumulation REGISTER

23.3 Voltage

Base quantity Phase reference Default unit


voltage L1_N V voltage L2_N V voltage L3_N V voltage L1_L2 V voltage L2_L3 V voltage L3_L1 V voltage none V

Important rule:

  • Phase-to-neutral and line-to-line voltages are different semantic measurements.
  • A point with phase_ref: L1 should not be treated as equivalent to phase_ref: L1_N unless the device profile explicitly declares that equivalence.

23.4 Current

Base quantity Direction Phase reference Default unit


current import L1 A current import L2 A current import L3 A current export L1 A current export L2 A current export L3 A current none N A

23.5 Frequency

Base quantity Default unit


frequency Hz

Frequency is normally not phase-specific.

23.6 Battery and Storage

Base quantity Default unit Notes


state_of_charge % Battery charge level state_of_health % Battery health battery_voltage V Battery terminal voltage battery_current A Battery current battery_power W Battery power charge_limit W or A Unit must disambiguate discharge_limit W or A Unit must disambiguate

23.7 Solar and Inverter

Base quantity Default unit


pv_voltage V pv_current A pv_power W dc_bus_voltage V inverter_temperature degC inverter_status enum

23.8 Thermal

Base quantity Location Default unit


temperature internal degC temperature external degC temperature battery degC temperature inverter degC

23.9 Status and Alarms

Base quantity Default type


device_status enum alarm_status enum or bit field fault_code enum or integer warning_code enum or integer communication_status enum

24. Measurand Aliases

ModDef should support aliases to map external naming systems to the ModDef semantic tuple model.

Example:

measurand_aliases:
- alias: Energy.Active.Import.Register
source: OCPP_1_6
maps_to:
base_quantity: energy_active
direction: IMPORT
accumulation: REGISTER

Example:

measurand_aliases:
- alias: Voltage.L1-N
source: OCPP_1_6
maps_to:
base_quantity: voltage
phase_ref: L1_N

This allows ModDef to be compatible with OCPP terminology without making OCPP the core namespace.

25. Measurand Validation

Linter and compiler validation rules:

  • A point may have zero or one measurand annotation.
  • A device profile must not expose duplicate semantic measurands unless explicitly allowed.
  • If duplicate measurands are allowed, they must differ by location, phase reference, context, or role.
  • The point unit must be compatible with the measurand canonical unit.
  • The point value type must be compatible with the measurand expected type.
  • Phase references must be valid for the base quantity.
  • Line-to-line voltage values must use L1_L2, L2_L3, or L3_L1.
  • Line-to-neutral voltage values must use L1_N, L2_N, or L3_N.
  • Current and per-phase power normally use L1, L2, or L3, not L1_N.
  • Frequency should not be phase-specific unless explicitly allowed by a profile extension.

Example linter messages:

MDE101: Unknown measurand base_quantity 'active_powr'.
MDE102: Point 'voltage_l1' uses unit 'A', expected unit compatible with 'V'.
MDE103: Duplicate measurand voltage/L1_N in device profile 'meter-a'.
MDW104: Point 'frequency_l1' uses a phase reference for frequency.

26. Generated Measurand APIs

Generated client libraries should expose both generic and convenience accessors.

26.1 Generic API

Python example:

device.get_measurand(
base_quantity="voltage",
phase_ref="L1-N"
)

Go example:

device.GetMeasurand(ctx, moddef.MeasurandQuery{
BaseQuantity: moddef.Voltage,
PhaseRef: moddef.PhaseL1N,
})

26.2 Convenience API

Python:

device.get_frequency()
device.get_voltage(phase="L1-N")
device.get_active_power(direction="import", aggregation="total")
device.get_energy_active(direction="import", accumulation="register")

Go:

device.Frequency(ctx)
device.Voltage(ctx, moddef.PhaseL1N)
device.ActivePower(ctx, moddef.Import, moddef.Total)
device.EnergyActive(ctx, moddef.Import, moddef.Register)

26.3 Unsupported Measurands

If a measurand is not supported, clients must raise or return a standard error.

Python:

raise MeasurandNotSupported("voltage", phase_ref="L1-N")

Go:

return 0, moddef.ErrMeasurandNotSupported

26.4 Ambiguous Measurands

If multiple points match a query, the client must not choose silently.

It must either:

  • Return an ambiguity error.
  • Require additional query qualifiers.
  • Use a profile-defined default if explicitly declared.

27. Protobuf Schema Draft

This schema is a starting point for development. Field numbers are draft and may change before v1.0. It reflects v0.4 (it now also includes the §11 write semantics, §15 string encoding, §14 composed kind, §7.2 function-code constraints, §16 firmware_versions, and §17 overlay flag that the v0.3 draft described in prose but omitted from the schema, plus all v0.4 additions).

The canonical schema is the Protobuf definition in moddef/proto/moddef/v1:

The full schema is omitted here to keep the rendered spec readable; the .proto files above are the source of truth.

28. Validation Rules

A compliant ModDef document must satisfy the following.

28.1 Structural Validation

  • Document id must be present.
  • Document version must be present.
  • Device profile ids must be unique.
  • Point ids must be unique within a device profile.
  • Enum ids must be unique after imports are resolved.
  • Struct ids must be unique after imports are resolved.
  • Measurand ids must be unique after imports are resolved.

28.2 Import Validation

  • All imports must resolve.
  • Import checksums must match if specified.
  • Import cycles must be detected.
  • Name collisions must be rejected unless aliased.

28.3 Mapping Validation

  • Register ranges must not overlap unless explicitly declared as overlays.
  • Points inside a register block must fall within the block range.
  • Multi-word values must define byte order and word order, or emit a warning.
  • Bit index must be valid for the mapped width.
  • Coil and discrete input mappings must not define length_words.

28.4 Type Validation

  • Storage type and value type must be compatible.
  • Enum references must resolve.
  • Struct references must resolve.
  • String storage must define length and encoding.
  • Scale denominator must not be zero.

28.5 Access Validation

  • Read-only points must not be generated with write methods.
  • Write-only points must not be generated with read methods unless explicitly allowed as unsafe raw access.
  • Command points must define write semantics.

28.6 Measurand Validation

  • Measurand references must resolve if using measurand_id.
  • Inline semantic tuples must use known base quantities unless vendor extension mode is enabled.
  • Phase references must be compatible with base quantity.
  • Units must be compatible with the canonical unit.
  • Duplicate semantic measurands must be rejected unless qualified or explicitly allowed.

28.7 v0.4 Construct Validation

  • transform must set at most one of scale and scale_ref.
  • A scale_ref / selector_ref / available_if / count_ref target must resolve to a point in the same device profile (MDE407, reserved).
  • A point whose value_type.primitive is DATETIME must define datetime.
  • A point whose value_type.kind is flags must define at least one bit.
  • na_values raw patterns must fit the decoded storage width.
  • A RegisterField (fields) range must lie within the mapped window and fields must not overlap unless explicitly allowed.
  • A block with discovery.kind = SUNSPEC must use model_relative_offset on its points (not absolute offset).
  • stride_words must be ≥ the element width for repeating arrays.

29. Tooling Specification

29.1 CLI Tool

Command:

moddef

Required commands:

moddef build
moddef lint
moddef fmt
moddef convert
moddef pack
moddef gen
moddef inspect

29.1.1 build

Builds .moddef.yaml or .moddef.json into .moddef.

moddef build device.moddef.yaml

29.1.2 lint

Validates the document and reports errors and warnings.

moddef lint device.moddef.yaml

29.1.3 fmt

Rewrites YAML or JSON into canonical formatting.

moddef fmt device.moddef.yaml

29.1.4 convert

Converts between supported encodings.

moddef convert device.moddef.yaml device.moddef.json
moddef convert device.moddef.json device.moddef

29.1.5 pack

Builds a reusable package.

moddef pack ./stdlib/measurands

29.1.6 gen

Generates client code.

moddef gen --lang python --input device.moddef.yaml --out ./generated

29.1.7 inspect

Prints a human-readable summary.

moddef inspect device.moddef

29.2 Exit Codes

Code Meaning


0 success 1 validation error 2 parse error 3 import resolution error 4 generation error 5 internal error

30. Linter Rules

Recommended linter rule categories:

30.1 Errors

  • Overlapping mappings.
  • Unknown enum reference.
  • Unknown measurand reference.
  • Invalid scale denominator.
  • Invalid phase reference.
  • Unit incompatible with measurand.
  • Unsupported storage and logical type combination.

30.2 Warnings

  • Missing description.
  • Missing endianness for multi-word point.
  • Unknown unit string.
  • Unused enum.
  • Unused import.
  • Duplicate human-readable names.
  • Suspicious 1-based address converted to 0-based.

30.3 Linter Configuration

File:

.moddeflintrc.yaml

Example:

rules:
MDW001:
severity: error
MDW_DESCRIPTION_MISSING:
severity: warning

31. Code Generation

Generated code should provide:

  • Typed point access.
  • Generic point access by id.
  • Generic measurand access.
  • Convenience measurand methods.
  • Enum types.
  • Decode and encode functions.
  • Optional Modbus transport wrappers.

Supported target languages:

  • Python
  • Go
  • TypeScript
  • C
  • C++
  • Rust

32. Client Library Architecture

Client libraries should be layered.

32.1 Core Model

  • Loads ModDef documents.
  • Resolves imports.
  • Provides metadata access.

32.2 Codec Layer

  • Decodes Modbus register responses into typed values.
  • Encodes write values into Modbus payloads.
  • Handles endianness, scaling, enum mapping, strings, bit fields, and composed values.

32.3 Transport Adapter

Defines an interface for Modbus operations:

  • read_coils
  • read_discrete_inputs
  • read_input_registers
  • read_holding_registers
  • write_single_coil
  • write_multiple_coils
  • write_single_register
  • write_multiple_registers

32.4 Device Facade

Generated high-level API for a device profile.

Example:

meter = ExampleMeter(client)
meter.get_frequency()
meter.get_voltage(phase="L1-N")
meter.get_energy_active(direction="import", accumulation="register")

33. Testing and Compliance

A compliance suite should include:

  • Golden files.
  • YAML to JSON equivalence tests.
  • JSON to binary equivalence tests.
  • Binary decoding tests.
  • Invalid document fixtures.
  • Linter fixture tests.
  • Code generation tests.
  • Cross-language decode/encode tests.
  • Optional hardware-in-loop tests.
  1. Define Protobuf schema.
  2. Implement parser and canonical model.
  3. Implement YAML and JSON conversion.
  4. Implement binary .moddef writer and reader.
  5. Implement import resolution.
  6. Implement linter.
  7. Implement codec in one reference language.
  8. Implement core measurand library.
  9. Implement generated clients for one language.
  10. Add SunSpec package mapping.
  11. Add additional language generators.
  12. Publish compliance suite.

35. External References

This section is informational.

  • Modbus Application Protocol Specification, for protocol-level address spaces and function codes.
  • SunSpec Modbus models, for solar, inverter, and storage examples.
  • OCPP measurands and phase values, for semantic measurement naming inspiration, especially phase references such as L1, L2, L3, N, L1-N, L2-N, L3-N, L1-L2, L2-L3, and L3-L1.

36. Design Notes

36.1 Why Not Use OCPP Measurands Directly?

OCPP measurands are useful and well known in EV charging, but they are domain-specific.

ModDef should not make EV charging terminology the universal core model. Instead, ModDef uses a semantic tuple model and provides OCPP-compatible aliases.

36.2 Why Not Encode voltage_l1_n as a Separate Core Measurand?

A flat list becomes hard to scale.

Instead of defining every combination as a separate name:

voltage_l1_n
voltage_l2_n
voltage_l3_n
voltage_l1_l2

ModDef defines:

base_quantity = voltage
phase_ref = L1_N

This is easier to validate, query, and generate.

36.3 Why Keep Protobuf Internal?

Protobuf provides a stable schema and compact binary encoding.

End users can still work with YAML and JSON, while tool authors can generate code and binary files from a canonical schema.

37. Revision History

v0.4 (2026-06-04)

Driven by validating v0.3 against 136 real device Modbus specifications across energy meters, solar inverters, battery storage, EV chargers, and HVAC heat pumps. The full analysis (coverage, per-device verdicts, and the ranked gap list) is in research/MODDEF-GAP-ANALYSIS.md; per-file reports are in research/_gap_reports/. Of 90 real Modbus register specs, 16 were fully expressible in v0.3 and 91 were partial, collapsing to the ~10 recurring constructs added here.

Added (blockers, without which common devices decode incorrectly):

  • Dynamic (register-referenced) scaling (§10.4, ScaleRef): value = raw · 10^SF where SF is another register. Mandatory for SunSpec; also used by Eaton IQ, Janitza, Carrier. Highest-impact change; unblocks essentially every SunSpec device.
  • Unavailable / sentinel values (§8.4, na_values): declare raw patterns such as 0x8000/0xFFFF/NaN that mean "no data" rather than a reading.
  • Dynamic / discovered addressing (§7.3, Discovery): SunSpec SunS model-chain walk and model-relative offsets.
  • Conditional point presence (§17.1, available_if).

Added (important, broad and cross-domain):

  • Register-field structs (§13.1, RegisterField): typed sub-fields with their own type/scale/unit (HIWORD/LOWWORD, packed BCD, version tuples, IPv4).
  • Flag sets (§13.2, FLAGS): multi-flag status registers.
  • Date/time values (§8.5, DATETIME + DateTimeSpec): epoch s/ms, packed BCD, split fields.
  • Repeating arrays with stride / count reference (§14.1, stride_words + count_ref): multi-string PV, per-cell battery.
  • Value/scale/unit selected by another register (§10.5, SelectorRef).

Added (moderate / localized):

  • Extended storage widths U24, U48, S48 and a sign_magnitude flag (§8.2).
  • Distinct write encoding (§11.5) and packed command arguments (§11.6).

Schema consistency fixes (prose features the v0.3 §27 draft had omitted): write semantics (WriteSemantics/WriteConstraints), string encoding (StringEncoding), composed kind, per-point function-code constraints, DeviceProfile.firmware_versions, and RegisterBlock.overlay are now present in the §27 schema.

Explicitly out of scope (documented in §3, not added): non-Modbus transports: Tesla (DNP3/REST), sonnen (REST), Pylontech US (proprietary ASCII), Vaillant/Bosch (eBUS/EMS), Easee/Zaptec/EVBox/ChargePoint (OCPP/REST), and BACnet-only devices. ModDef remains a Modbus description format.

v0.3

Initial public draft: document model, four address spaces, logical/storage type split, endianness, rational scaling, access modes, enums, bit fields, composed values, strings, device profiles, register blocks, variants, imports and packages, the core measurand tuple system and catalog, the §27 Protobuf schema draft, validation/linter rules, tooling, and compliance testing.