Skip to main content

Transforms & scaling

A raw register rarely is the engineering value. A transform turns the decoded integer into the number you want, exactly.

Static rational scale and offset

The common case: value = raw × scale + offset, where scale and offset are exact rationals (numerator / denominator), never lossy floats.

# 2305 → 230.5 V
transform: { scale: { numerator: 1, denominator: 10 } }

# raw × 0.1 − 1 (a signed-range trick some inverters use)
transform:
scale: { numerator: 1, denominator: 10 }
offset: { numerator: -1, denominator: 1 }

Runtimes compute this with exact rational arithmetic (i128, bigint, fractions.Fraction, …) and surface a double. Encoding a write runs the inverse exactly.

Register-referenced scale factors (SunSpec)

SunSpec devices store a shared scale factor in a companion register: the value's real magnitude is raw × 10^SF, where SF is read live from another point. Use scale_ref:

# ac_power = raw × 10^(value of w_sf)
transform:
scale_ref: { point_id: w_sf, mode: POW10 }

mode: MULTIPLY covers the raw × ref / denominator variant. The runtime reads the referenced point automatically before decoding.

Selector cases

Some registers change meaning based on another register. For example, an energy counter's unit (Wh or kWh) can depend on a format flag. selector_ref picks a per-case scale and offset, falling back to the point's own transform when no case matches:

selector_ref:
point_id: energy_format
cases:
0: { scale: { numerator: 1, denominator: 1000 }, unit: kWh }
1: { scale: { numerator: 1, denominator: 1 }, unit: kWh }

Composed values

COMPOSED storage reconstructs mantissa × base^exponent from two sub-windows of the same register span. A few meters use this form for wide-dynamic-range readings. It is read-only; the mantissa and exponent offsets are part of the mapping.

All of these are defined once in the document and applied identically by every runtime, and the conformance suite pins them to the same numeric results.