Sensors declared on an asset
A sensor slot does not have to be added to every twin by hand. An asset can declare scalar sensors in its universal schema (the JSON that describes what the asset is; see Editing a universal schema), and every twin created from that asset inherits them through its capabilities (capabilities.sensors, see Sensor capabilities). A scalar sensor reports one number per reading: a temperature, a power draw, a fan speed. Unlike a camera or a lidar it has no field of view and needs no pose, so the 3D view draws nothing for it, and the twin editor’s sensor list shows it by name with its unit and without field-of-view controls. It does not block a simulation either: the checks that run before a simulation starts skip scalar sensors, since there is nothing to simulate for them.
An entry in the asset’s sensors list carries a type from the scalar vocabulary (the list on the dataset page), the slot id under parameters.id and, optionally, a unit under parameters.unit. A position inside the asset is optional:
cyberwave/server-2u, a generic two-unit rack server, declares fourteen such slots:
The names are generic on purpose: they say what a server measures, not what a vendor calls it. A twin made from the asset lists all fourteen slots in its Data sources and Live values sections from the start; the translation from a dataset’s channel names to these slots happens in the binding (see Bind from dataset). A twin can still add slots of its own in
metadata.sensors for readings its asset does not declare (see Declare sensor slots on the twin); both sets are merged into the twin’s sensor list.
The type gate
Both sides of a binding carry a type: the slot (declared on the asset or the twin) and the channel (from the recipe’ssignals annotations, generic when none matched). Cyberwave accepts a binding only when the two types are the same, or when one side is generic or has no type. A temperature slot takes a temperature channel and refuses a power channel, so a wrong pick in a list of hundreds of channels does not go unnoticed.
A mismatch is sometimes intended, for example a ratio channel that should drive a utilization slot. It can be allowed explicitly, and only explicitly:
- In the Data sources section the channel search is filtered to the slot’s type by default; All types lifts the filter. Choosing a channel of another type asks for confirmation:
<channel> carries <type>, this sensor measures <type>. Bind anyway?Once confirmed, the binding is stored with the override and the row shows an amber type mismatch badge from then on. - Over the API and from Python, the binding entry carries
"allow_type_mismatch": true.
PUT /api/v1/twins/{uuid} with data_sources in the metadata answers 422 with the reason for each failing slot, named as <twin>.<slot>, for example rack-48-s00.inlet_temperature: slot 'inlet_temperature' measures temperature but channel 'ipmi/total_power/node=960' carries power; choose a channel of the same type or allow the mismatch explicitly. The same request also checks that the dataset exists and has finished processing (422, dataset missing) and that the channel is in the dataset’s catalog (422, unknown channel). Nothing is written when one entry fails. Entries in the legacy shape and entries without a dataset are left as they are.
Units are not converted. The check compares kinds, not units: a channel in °F on a slot declared in °C passes, and its values are stored and shown as they come. When the units differ, the row says channel unit <u1>, sensor unit <u2>, values are not converted. Convert at import instead, with scale and offset in the recipe’s signal annotations, or declare the slot in the channel’s unit.
The environment’s sensor-sources report, GET /api/v1/environments/{uuid}/sensor-sources, lists every bound slot with type_mismatch (the types differ and the mismatch was allowed), unit_mismatch and allow_type_mismatch, and each twin’s identifier (thing_id). A switched-on binding whose types differ without the override, which is only possible for bindings made before the check existed, is refused by the recorded data feed: Play answers with a message naming the sensor as <twin>.<slot>. Attach the channel again in Edit mode, confirming the mismatch, or pick a channel of the slot’s type.
Identifier in datasets
In a dataset, a channel id is the signal followed by the thing: what is measured, then where (ipmi/ambient/node=960, see the vocabulary). A twin that stands for one such thing gets that string as its identifier in datasets (metadata.thing_id). With it, Cyberwave knows that ipmi/ambient/node=960 is the inlet temperature of this server and ipmi/ambient/node=961 belongs to its neighbour, and it can bind a twin’s slots without you searching every channel.
Set it in Edit mode in the twin’s Data sources section, field Identifier in datasets, or over the API with PUT /api/v1/twins/{uuid} and the body {"metadata": {"thing_id": "node=960"}} (top-level metadata keys are merged, so the other keys are kept). From Python, cw.twins.set_thing_id(twin.uuid, "node=960") does the same. The build script in From Python sets it for every server it creates. The identifier is free text and must match the thing part of the channel ids exactly; the report of GET /api/v1/environments/{uuid}/sensor-sources shows it per twin.
A dataset whose data has no entity column names its channels by the signal alone (weather/temp). The template below still binds such a channel: when the signal joined with the twin’s identifier is not in the catalog but the signal alone is, the signal alone is used. The twin still needs an identifier to take part.
Bind from dataset
Once a twin has an identifier, one click binds every slot the dataset can serve. In Edit mode, the Data sources section has a Bind from dataset button: pick one of your time-series datasets and, when the twin has docked children (twins attached to one of its links, such as the servers in a rack), tick Also docked children to bind them in the same go. The button uses the dataset’s slot template (next section); a dataset without one says so, and you attach channels by hand or pass a template over the API. For every twin, and for every scalar slot the template names, Cyberwave joins the template’s signal and the twin’s identifier into a channel id (ipmi/ambient and node=960 give ipmi/ambient/node=960), looks it up in the dataset’s catalog, applies the type gate and writes the binding. Each twin’s bindings are saved once, as a whole. The report, shown under the button, lists per twin its identifier, how many slots were bound and the slots skipped with the reason (the API response below also names the channel of every bound slot):
Slots the template does not mention are left as they are. The totals at the end of the report count bound and skipped slots over all twins. The form’s Use recorded data box, ticked by default, sets the switch of every new binding (
use_recorded over the API); look over the rows afterwards and switch off the ones that should keep their live input.
The same operation over the API, with edit access to the environment like editing a twin:
Slot template in the recipe
The template that Bind from dataset uses is part of the dataset: the optionalslots entry of cyberwave-timeseries.json (see the recipe reference) maps slot ids to signals. Signals, not channels: the thing part comes from each twin’s identifier, so one template serves every server of every rack.
metadata.timeseries.slot_template. A dataset imported without one can still be bound over the API or from Python by passing template in the request.
The reference recipe for the Marconi100 export, cyberwave-python/examples/timeseries_replay/recipes/marconi100.json, maps the slots of cyberwave/server-2u this way:
outlet_temperature has no Marconi counterpart and stays unbound.
From Python
The Python SDK wraps the calls above. Twins, datasets, environments and feeds are given by id (theuuid of the object). Errors raise the SDK’s usual API exception with the backend’s message, for example the reason of a refused binding.
Together they build a rack in a few lines. The script below creates rack 48 of the Marconi100 data: the rack twin from
cyberwave/server-rack-42u, twenty cyberwave/server-2u twins docked to its server_rack_42u_link link at 2U pitch from the bottom up, each with its node number as identifier, and binds them with the dataset’s slot template. The API key comes from CYBERWAVE_API_KEY.
cyberwave-python/examples/datacenter_rack/build_rack.py, which finds the rack and the servers by name and, on a re-run, also re-docks the servers and rewrites their identifiers:
rack-48 and rack-48-s00 to rack-48-s19, docked bottom to top, sets the identifiers node=960 to node=979, binds the dataset with the chosen template (the Marconi100 one, a JSON file of your own, or else the dataset’s) and prints the report. --as-kit also registers the kit cyberwave/server-rack-42u-20x2u (the rack with twenty servers at the same offsets) in the Catalog.
If you prefer the app, add the kit with Add to environment from the Catalog (Asset kits), give each server its Identifier in datasets, and press Bind from dataset on the rack with Also docked children ticked.
Next steps
Time-series datasets
Package sensor logs, search channels and attach a channel by hand.
Recorded data feed
Play the bound channels into the twins in Live and Simulate.
Asset kits
Reuse a rack with its servers as one docked assembly.
Python SDK
Install the SDK and authenticate.