# title
Lattice — Topology & Capability Document

# description
An OpenAPI-style, vendor-publishable document describing a site's device topology (a graph of nodes connected by typed directed relationships) and the read/control capabilities each node offers, with concrete data-driven bindings and ranked access paths. A producer (a cloud integration or the gateway) emits a conformant doc; any consumer ingests it generically with no per-provider code. Scale-free: the same constructs describe a cell, a device, a site, or a city.

# properties.topologyVersion.description
Version of THIS specification the doc conforms to (semver).

# properties.docVersion.description
Monotonic instance version of THIS document. Bumps on any change, including capability `ref` (re)assignment. The data-plane protobuf carries `doc_version`; a consumer decodes telemetry/encodes control against the doc of the matching docVersion (re-reading the retained topology on an unknown version).

# properties.id.description
Stable identifier for this document / site / fragment.

# properties.scope.description
'fragment' = one producer's slice (the consumer merges fragments by node identity); 'site' = an already-merged graph.

# properties.deviceTypes.description
Reusable device-type descriptors (the components/$ref equivalent): a fingerprint plus capability/binding templates, referenced by node.deviceType.

# $defs.producer.properties.provider.description
Provider/transport identity backing this producer's access paths.

# $defs.producer.properties.authority.description
Precedence in a merge: higher wins; docVersion recency breaks ties. On-device discovery = low (0); installer/cloud overlays higher.

# $defs.node.properties.id.description
Stable identity (e.g. a serial). THE merge key across producers — the same id from two producers is one node with both their access paths.

# $defs.node.properties.deviceType.description
Optional key referencing a deviceTypes[] descriptor.

# $defs.node.properties.accessPaths.description
Ways this producer can reach the node. The consumer merges + ranks all producers' access paths to the same id.

# $defs.node.properties.removed.description
Overlay tombstone: when true this node (keyed by id) is removed from the merged result; only `id` is required.

# $defs.nodeKind.description
Domain-agnostic node class. Vendor/domain extensions use a namespace prefix, e.g. 'x-acme:thing'. 'structural' = a pure aggregator/virtual node (e.g. a plant or a city) with no telemetry of its own.

# $defs.attributes.description
Free-form node attributes referenced by routing, grouping and constraints.

# $defs.geo.description
Optional geospatial location — needed at building/district/city scale.

# $defs.accessPath.description
A way to reach this node's capabilities via a provider/transport.

# $defs.accessPath.properties.preference.description
Higher = preferred; the consumer fails over to the next-highest on unavailability.

# $defs.accessPath.properties.removed.description
Overlay tombstone: when true this access path (keyed by id) is removed; only `id` is required.

# $defs.aggregate.description
When (and with what priority) this node serves as the aggregate read/control point for its children, instead of routing to each child. Replaces hardcoded primary-selection with data.

# $defs.aggregate.properties.minChildren.description
Qualifies only if the configured child-count over `over` >= this. e.g. EMS=0 (always primary), Gateway=2 (only when it fronts >1 unit).

# $defs.aggregate.properties.priority.description
Higher wins when several aggregators qualify (e.g. EMS > Gateway).

# $defs.capabilityOffer.description
A capability this node offers on a given access path, with read and/or control binding(s), aggregation semantics and constraints. The same capability may appear multiple times on one node (one per access path) — the consumer ranks them by access-path preference (this is how 'control via gateway OR cloud, with fallback' is expressed).

# $defs.capabilityOffer.properties.ref.description
Stable small integer (the data-plane `cap_ref`) identifying this (node, capability) on telemetry/control. Keyed by node+capability — multiple access-path offers for the same capability SHARE one ref (the consumer picks the access path). Stable within a docVersion.

# $defs.capabilityOffer.properties.accessPath.description
id of the node's accessPath that serves this offer.

# $defs.capabilityOffer.properties.groupBy.description
Attribute name partitioning a multi-element read into per-group results (e.g. 'phase' for grid voltage).

# $defs.capabilityOffer.properties.shape.description
How the device accepts a control intent for this capability: a numeric value effective now (setpoint), an enum/on-off effective now (switch), or a time-windowed plan applied atomically (schedule). Required on an offer that carries a `control` binding. A `schedule` offer also declares a `scheduleSpec` (its slot count, honoured slot fields, end-bound and default-mode needs); the plan a consumer sends is an ordered list of `$defs/scheduleSlot` (a control-time payload, not topology data).

# $defs.capabilityOffer.properties.controlGroup.description
Name of a coupled-write group. Capabilities on the same node sharing a controlGroup resolve and execute as ONE atomic operation (e.g. Solax (mode,target,power)); the resolver gathers all members' values, reading current state for any the consumer didn't supply. With ≥2 members, each declares a `groupSlot` saying where its value goes in the one operation.

# $defs.capabilityOffer.properties.tier.description
Conformance tier of this control offer: 1 = declarative binding (zero provider code, e.g. Modbus/CID); 2 = provider-implemented (imperative: schedule rebuild, coupled mode call, OAuth cloud write). Required on an offer that carries a `control` binding.

# $defs.capabilityOffer.properties.removed.description
Overlay tombstone: when true this offer (keyed by capability+accessPath) is removed; only `capability` is required (a bare-capability tombstone with no `accessPath` targets the access-path-less derived offer).

# $defs.groupSlot.description
How a controlGroup member's value is placed into the group's single operation. Exactly one of `field` (a named field in the coupled op's payload — e.g. Solax soc_target_control_mode's 'targetSoc') or `bits` (a bit-range of a shared register, written read-modify-write preserving other bits — e.g. Solis CID 636's storage-mode bitfield, where several capabilities each own bits). The member's own `control.transform` still applies (e.g. negate a discharge value into a shared signed field).

# $defs.groupSlot.properties.field.description
named field in the coupled operation's payload that carries this member's value.

# $defs.groupSlot.properties.bits.description
bit range [lsb .. lsb+width-1] this member owns in the shared register; the executor writes it read-modify-write, leaving other bits untouched.

# $defs.derived.description
A read value COMPUTED from OTHER capabilities on this node (not read from a register) — used in place of a `read` binding. Deterministic function of declared inputs only (e.g. load = pv − battery − grid); NEVER consumer-side analytics (calibration, learning, forecasting). Inputs reference sibling capabilities by `class.function`; the resolver gathers their current values and evaluates the op. No cycles.

# $defs.derived.oneOf[].description
Weighted sum: value = Σ weight_i × value(input_i). Covers load_power, battery net flow.

# $defs.derived.oneOf[].description
Ratio: value = factor × value(num) / value(den). Covers e.g. usable capacity = remaining × 100 / soc.

# $defs.scheduleSpec.description
On a `shape: schedule` offer: the device's schedule SURFACE — enough for a consumer to build a valid plan with no per-vendor knowledge. The plan it sends is an ordered list of `scheduleSlot`s (a control-time payload, not topology data).

# $defs.scheduleSpec.properties.maxSlots.description
maximum number of slots the device accepts (e.g. Fox 8).

# $defs.scheduleSpec.properties.slotFields.description
optional per-slot refinements this device honours, beyond the always-present {start, end, mode}.

# $defs.scheduleSpec.properties.endBound.description
whether a slot's `end` time is exclusive (default) or inclusive (e.g. Fox treats the end minute as inclusive).

# $defs.scheduleSpec.properties.requiresDefaultMode.description
true if the device needs a default `battery.mode` for time not covered by any slot.

# $defs.scheduleSlot.description
One segment of a `schedule`-shape control plan — a CONTROL-TIME payload a consumer sends, not part of a topology document. `start`/`end` are HH:MM local (end interpreted per the offer's scheduleSpec.endBound); `mode` is a `battery.mode` scheme; optional refinements must be a subset of the offer's scheduleSpec.slotFields.

# $defs.scheduleSlot.properties.end.description
HH:MM (local); exclusive or inclusive per scheduleSpec.endBound

# $defs.scheduleSlot.properties.mode.description
battery.mode scheme for this window (e.g. force_charge, self_use)

# $defs.capabilityName.description
Capability identity, qualified by device class as 'class.function' (the merge/resolve key). The class reflects the function's domain, not the node's kind (a gateway may offer 'battery.charge_power_limit' AND 'meter.grid_power'). Qualifying by class prevents collisions a bare name causes — e.g. a battery's charge power (W) vs an EV charger's charge current (A) are 'battery.charge_power_limit' and 'ev_charger.charge_current_limit', never grouped. Vendor/domain extensions namespace further (e.g. 'x-acme:foo'); a capability name MUST NOT name a vendor or device. Core set in examples.

# $defs.binding.description
The concrete 'how to read/write this on this access path': protocol, address, op, encoding and a value transform. An adapter executes it; the model needs no per-model code. This is what replaces hardcoded register maps.

# $defs.binding.properties.address.description
Register number/range, API endpoint/field, OCPP action, etc.

# $defs.binding.properties.count.description
# elements/registers — vector reads (per-unit SOC) or slot start+end.

# $defs.binding.properties.readModifyWrite.description
true if executing this control op requires reading current device state, merging the new value, and writing the whole object back (e.g. Fox schedule rebuild) — the executor read-modify-writes rather than writing a scalar.

# $defs.transform.description
Named, parameterised value transform (raw <-> engineering units). Core kinds are GENERIC; vendor-specifics live in a binding's parameters (e.g. a `ref` to a node parameter), or — as a last resort — a namespaced `x-<vendor>:` extension kind. A vendor name MUST NOT appear as a core kind (same rule as capability names). Optional `round` selects the division rounding mode (default truncating).

# $defs.transform.properties.kind.description
A core registry kind, or a namespaced `x-<vendor>:` extension.

# $defs.transform.properties.round.description
Rounding mode for a dividing kind (`ratio`/`affine`). `trunc` (default) = integer division toward zero (back-compatible); `half_up` = round half away from zero ((x + den/2)/den for positive); `half_even` = round half to nearest even (banker's). Ignored by non-dividing kinds; `pipeline` steps carry their own.

# $defs.transform.properties.onRefUnavailable.description
Control-write behaviour when a `ref` parameter can't be resolved at runtime (e.g. battery capacity not yet read). `zero` (default) = fail-closed, yield no value / 0. `max` = fail-open: emit this transform's `max` value (the unrestricted maximum) rather than an under-scaled guess — e.g. a battery rate cap that writes 'unrestricted' until capacity is known. Requires `max` to be set (a conforming executor must know what to emit). A generic, vendor-neutral degradation policy.

# $defs.valueOrRef.description
A transform parameter: a literal number, or a reference to a declared node parameter with an optional constant factor.

# $defs.valueOrRef.oneOf[].properties.ref.description
Name of a node `parameters` entry; resolved at execution time.

# $defs.valueOrRef.oneOf[].properties.factor.description
Constant multiplier on the referenced value (e.g. capacity × 0.5).

# $defs.nodeParameters.description
Named node values a transform `ref` resolves against. Each value is EITHER a static literal number (known at config/discovery time, e.g. rated power) OR `{ "source": "<capability>" }` — resolved at execution time from that capability's latest reading (e.g. capacity from `battery.capacity`). A `source` capability MUST itself be resolvable without a `ref` back to a parameter (no cycles). If a sourced value is unavailable at runtime, a transform that refs it applies its `onRefUnavailable` policy (fail-closed by default).

# $defs.nodeParameters.properties.capacity.description
Wh — typically { source: "battery.capacity" }

# $defs.paramValue.description
A node parameter: a static literal number, or a runtime value sourced from a capability.

# $defs.paramValue.oneOf[].properties.source.description
capability whose latest value supplies this parameter at runtime

# $defs.reducer.description
How multiple read sources (or vector elements) combine. 'representative' = redundant value, read one. 'spread' = max-min (e.g. cell imbalance).

# $defs.distribution.description
How one control intent maps onto multiple targets.

# $defs.relationType.description
Directed edge, read subject->object, with an implied named inverse.

# $defs.constraints.description
Bounds/valid-set/interlocks resolution must respect. A bound may be static (a number), a derived-expression sentinel string ('rated', 'min(capacity/2, rated)'), or runtime-sourced from a device-reported capability ({ source: '<capability>' }) — e.g. Fox's fdpwr_max / fdsoc_min read from the device and used to clamp writes.

# $defs.constraints.properties.max.description
A number, a derived-expression string ('min(capacity/2, rated)'), or { source: capability } resolved at runtime.

# $defs.constraints.properties.scope.description
site = e.g. max grid import, G98/G99 export limit.

# $defs.constraintBound.description
A constraint bound: a static number, a sentinel/derived-expression string, or a runtime value sourced from a device-reported capability.

# $defs.constraintBound.oneOf[].properties.source.description
capability whose latest value supplies this bound at runtime (e.g. a device-reported power cap)

# $defs.deviceType.description
Reusable descriptor: a fingerprint (recognition rules — the discovery schema as data) plus capability/binding templates and a default aggregate. Instances reference it via node.deviceType. Keyed by (make, model, firmware); a firmware change is a new descriptor version, not a per-node rewrite.

# $defs.fingerprint.description
How to recognise a device as this type (used by discovered producers, e.g. the gateway).

# $defs.fingerprint.properties.dtcHi.description
Device-type-code high byte, e.g. '0x80'.

# $defs.fingerprint.properties.probe.description
e.g. an input register that must be > 0.

# $defs.relationship.properties.removed.description
Overlay tombstone: when true this relationship is removed from the merged result.