Data Model
Data Model
Section titled “Data Model”Aether models the physical plant in three layers: products (device-type templates), instances (individual devices), and points (single measurable or actionable quantities). The central design invariant is that an instance is a pure thing-model: it holds logical structure plus current values, and nothing else. Connectivity, alarms, and routing live in separate datasets that reference the instance without ever being copied onto it.
Three layers
Section titled “Three layers”Product — a template describing what points a device type has. Defined
in Product (libs/aether-config/src/automation.rs): a unique product_name, an
optional parent_name for the type hierarchy (Station → ESS → Battery/PCS,
and so on), plus three point lists:
measurements— measurement point definitions (id, name, unit, description)actions— action point definitions (id, name, unit, description)properties— property templates (static configuration values, e.g. rated power)
The kernel product library is empty by default. Validated active Packs provide
JSON model assets (the Energy Pack owns its models under
packs/energy/models/), where the same three lists appear as M, A, and P
and the hierarchy as name / pName. A site may add an explicit custom
directory. Product candidates pass the same confinement, regular-file, size,
JSON, and duplicate-name checks in validation and at runtime.
Instance — one physical device. Defined in Instance / InstanceCore
(libs/aether-config/src/automation.rs):
instance_id(numeric, unique) andinstance_nameproduct_name— which template this device instantiatesparent_id— position in the site topology (None for root instances)properties— concrete values filled in for the product’s property templates- optional
created_at— a creation timestamp (a bookkeeping fact about the record itself, not aggregated runtime state)
That is the complete field list. There is no status, health, online,
degraded, or alarm_state field — see the purity rule.
Point — a single measurable or actionable quantity, identified by a numeric id that is unique within its product and its point list. A point on the acquisition side is something the device reports (state of charge, breaker position); a point on the command side is something the system can set (start command, power setpoint).
Point types
Section titled “Point types”Channel-side points use the four-type classification defined by PointType
in libs/aether-core/src/types.rs (re-exported through aether-model). The
enum documentation calls this the standard IEC “Four Remote” classification;
each variant carries a serde alias for the Chinese-standard code (YC/YX/YK/YT).
| Type | Name | Signal kind | Direction | Write owner |
|---|---|---|---|---|
T |
Telemetry | Analog measurement (YC) | device → system | io |
S |
Signal | Digital status (YX) | device → system | io |
C |
Control | Digital command (YK) | system → device | automation |
A |
Adjustment | Analog setpoint (YT) | system → device | automation |
PointType provides the category predicates the codebase uses everywhere:
is_measurement() is true for T and S, is_action() for C and A,
is_analog() for T and A, is_digital() for S and C.
Write ownership is enforced by construction at the port and composition
boundaries. IO alone receives LiveStateWriter and publishes T/S acquisition
batches through ShmAcquisitionStateWriter. Automation submits typed C/A
commands through ShmDeviceCommandSink; it cannot obtain the acquisition
writer, and the sink rejects non-command point kinds before touching SHM.
On the instance side, the four channel types collapse into two roles, defined
by PointRole in libs/aether-model/src/types.rs:
M(Measurement) — data flows device → modelA(Action) — data flows model → device
Live values are addressed by typed SHM coordinates. Channel-to-model and model-to-channel mappings are durable configuration in SQLite and are loaded into in-process routing indexes. A custom mirror may choose its own external key shapes, but those keys are not part of the core data model.
The purity rule
Section titled “The purity rule”An instance holds logical structure plus current values, and nothing else.
The Instance struct has identity, hierarchy, properties, and point mappings.
No aggregated state of any kind — no status, no health, no online, no
alarm_state — exists on it.
This is a deliberate design decision, not an omission. Each candidate status field is actually a property of a different subsystem with its own writer and its own lifecycle:
- “Online” is a property of a communication channel, not a device. An instance’s points bind to channel points through the routing tables, and nothing forces them all onto one channel. An instance-level online flag would be a lossy aggregate of per-channel facts that io publishes in the channel-health SHM segment.
- “Has an active alarm” is a property of the alarm event stream, which alarm owns in its own tables. Alarms reference points by coordinates; the reference points one way only.
- “Last control write failed” is a property of a single call, and it surfaces in that call’s return value (see Consequences for UIs and agents).
Copying any of these onto the instance would create a second copy of a fact whose authoritative writer lives elsewhere. Second copies go stale, conflict with the original under partial failure, and force every writer to know about every consumer. Keeping the instance pure means each fact has exactly one writer and consumers join the datasets they need at read time.
Four orthogonal datasets
Section titled “Four orthogonal datasets”The runtime state of the system is split across four datasets. Each has one writer; none is derived from or copied into another.
| Dataset | Where | Meaning | Writer |
|---|---|---|---|
| Instance current values | Typed point slots in live-state SHM, resolved through the routing index | Thing-model values (a point may be NaN if never acquired) | io (M source values); automation (A, on action execution) |
| Channel connectivity | Channel-health SHM segment | Per-channel online/offline state and heartbeat | io |
| Alarm events | alarm SQLite tables alert and alert_event (services/alarm/src/db.rs) |
Event stream: trigger/recovery rows addressing points by service_type + channel_id + data_type + point_id (for instance points, service_type is "inst" and the id column holds the instance id) |
alarm |
| Routing configuration | SQLite mappings loaded into per-process indexes | Static point-to-point wiring between channel points and instance points | Configuration sync/API transaction |
The routing indexes are the join keys between the others: C2M maps a
channel point ({channel_id}:{T|S}:{point_id}) to an instance measurement
({instance_id}:M:{point_id}), and M2C maps an instance action
({instance_id}:A:{point_id}) back to a channel command point. Alarm rules
address values by the same coordinates. Instances
never reference alarms, connectivity, or routing back — the arrows point one
way.
NaN as a sentinel
Section titled “NaN as a sentinel”In the shared-memory plane, “no data has ever been written here” is encoded
in the value itself. Every SHM PointSlot
(crates/aether-dataplane/src/core/slot.rs) is created with both its value and
raw-value fields set to a quiet IEEE-754 NaN (the hardcoded bit pattern
SLOT_UNWRITTEN_BITS = 0x7FF8_0000_0000_0000). The first real write replaces
the sentinel with a finite double. This removes the historical ambiguity where
a zero-initialized slot was indistinguishable from a genuine reading of 0.0.
There is no side-channel quality flag in the cross-service data plane.
The 32-byte PointSlot layout is value, timestamp, raw value, seqlock
sequence, and dirty flag — nothing else. (io’s protocol layer does track
per-point quality codes internally in services/io/src/protocols/core/,
but they never cross the SHM boundary.) The value is the data; consumers must
check for NaN explicitly:
- SHM readers probe
PointSlot::is_unwritten()orf64::is_nan()on the returned value. - The rule engine (
libs/aether-rules/src/executor.rs) tracks which variables were unavailable inRuleReadOutcomeand skips evaluation rather than substituting 0.0 — otherwise a condition likecurrent < thresholdwould silently fire on missing data.
The rule for every consumer is the same: absence of a valid finite value is a first-class outcome you must handle, not an error state recorded somewhere else.
Consequences for UIs and agents
Section titled “Consequences for UIs and agents”Graying out a control button is the client’s join. To decide whether a control on instance 7 can currently reach its device, resolve the instance’s action point through the M2C routing index to a channel id, then read that channel from the channel-health SHM segment. The backend deliberately does not pre-join this onto the instance. automation performs the same join at write time before dispatching.
Control-write failures surface in the caller’s return value, never as
instance state. When the target channel is offline, automation rejects the write
with AutomationError::ChannelUnreachable (services/automation/src/error.rs); a
degraded dispatch path (SHM written but the notification to io failed)
fails with AutomationError::DispatchDegraded. Both reach HTTP through
AetherErrorTrait::http_status (libs/errors/src/lib.rs), where their
categories — ResourceBusy and Network respectively — both map to
HTTP 503; callers distinguish them by error code
(AUTOMATION_CHANNEL_UNREACHABLE vs AUTOMATION_DISPATCH_DEGRADED), and both are
retryable. In the rule engine, an action that cannot be resolved produces an
ActionResult with success: false and a NaN value via the executor’s
action_skipped path, attributed to the variable that caused the skip.
In every case the failure is information for that caller, that call — it is
reported, logged, and possibly retried, but nothing is written back onto the
instance. The next reader of inst:{id}:A sees the last accepted command,
not a failure flag.
Related pages
Section titled “Related pages”- System Architecture — services and how they communicate
- Shared Memory — the SHM plane in depth: slots, seqlock, writer ownership
- Data Flow — the uplink and downlink paths end to end
- Product Models — the product library and its domain meaning
- Safe Operations — why control writes are gated and how failures propagate