Skip to main content
Version: v1.1.0

Mapping XML unit codes and observed timestamps

A measurement needs a value, a unit, and a timestamp. This example maps the latest observation from one weather station to the WeatherObserved Smart Data Model. Each reading gets a unitCode and an observedAt, so later fetches can add observations for the same station instead of replacing earlier readings.

Get the data​

The feed is the latest surface observation for Ljubljana Bežigrad, from the Slovenian Environment Agency (ARSO):

curl --fail --location --output data/observation.xml "https://meteo.arso.gov.si/uploads/probase/www/observ/surface/text/sl/observation_LJUBL-ANA_BEZIGRAD_latest.xml"

Get the schema​

WeatherObserved is a published Smart Data Model. Download the catalog once:

cassiopeia sdm download

The record shape​

The document contains one station inside an envelope. Feed-level fields come first, followed by one metData element containing the observation. Since nothing repeats, Cassiopeia keeps the document as one record and exposes the measurements under metData. A reading pairs a value with its unit and description:

<metData>
<domain_id>65142878</domain_id>
<domain_lat>46.0658</domain_lat>
<domain_lon>14.5172</domain_lon>
<domain_title>LJUBLJANA/BEZIGRAD</domain_title>
<valid_UTC>21.08.2026 10:00 UTC</valid_UTC>
<t_var_unit>°C</t_var_unit>
<t>23</t>
<rh>80</rh>
<msl>1011</msl>
<ff_val>0.2</ff_val>
<dd_val>54</dd_val>
<vis_value>20</vis_value>
</metData>

The mapping reads each field as metData.<name>.

unitCode is a code, not a symbol​

The feed prints units as symbols such as °C, hPa, and m/s. NGSI-LD's unitCode is different: it uses the UN/CEFACT Common Code for the unit (ETSI GS CIM 009 v1.9.1 clause 4.5.2.2). Each field has a fixed unit, so the mapping uses a constant code:

temperature: {
source: "{{ metData.t }}",
type: "Property",
transformation: "float",
properties: {
unitCode: {
source: "CEL",
},
observedAt: {
source: "{{ metData.valid_UTC | replace(from='.', to='/') | replace(from=' UTC', to='') }}",
type: "Property",
transformation: "datetime",
},
},
},

unitCode and observedAt are declared under properties, like other attribute-level properties. NGSI-LD reserves those names, so they are emitted as qualifiers on the value rather than as ordinary nested properties. The codes used here are:

QuantityFeed symbolunitCode
temperature, dew point°CCEL
relative humidity% (as ratio)C62
atmospheric pressurehPaA97
wind speedm/sMTS
wind direction°DD
visibilitykmKMT

observedAt makes it temporal​

The feed's valid_UTC value is 21.08.2026 10:00 UTC, which datetime does not read directly. The mapping changes the dots to slashes and removes UTC, producing the supported form 21/08/2026 10:00, parsed as UTC. That instant becomes the reading's observedAt.

An observedAt makes the entity temporal. The station ID stays stable, while the timestamp keeps successive readings distinct. The gold-price example uses the same approach for a monthly series; here each fetch adds a new observation for the station. The model also requires dateObserved, which comes from the same instant.

Meeting the model​

WeatherObserved requires id, type, dateObserved, and location. The mapping supplies the timestamp and builds a point from the station's longitude and latitude. The model expects relativeHumidity as a ratio in [0, 1], while the feed reports a percentage, so the mapping divides it by 100. The result carries C62, the UN/CEFACT code for "one":

relativeHumidity: {
source: "{{ metData.rh | int / 100 }}",
type: "Property",
transformation: "float",
properties: {
unitCode: {
source: "C62",
},
},
},

The manifest​

{
version: "v1",
inputs: [
{
source: "data/observation.xml",
mapping: "weather.json5",
format: "xml",
},
],
output: {
target: "file",
directory: "out",
context: "none",
validation: {
mode: "fail-when-schema",
},
},
}

Run it​

cassiopeia map \
--manifest manifest.json5

The same run in a container mounts this directory at /data and makes it the working directory, so the paths do not change. For Podman, replace docker with podman and drop the --user line: rootless Podman already maps the container's root to your user.

docker run --rm \
--user "$(id -u):$(id -g)" \
--volume "$PWD:/data" \
--volume "$HOME/.cache/cassiopeia-examples/schemas:/var/lib/cassiopeia/schemas" \
--workdir /data \
ghcr.io/vela-tools/cassiopeia:v1.1.0 \
map \
--manifest manifest.json5

From the repository root, the runner downloads the dataset, runs the mapping, and checks the output in one step:

cargo run -- run 14
cargo run -- run 14 --runtime docker

Read the result​

The output contains one WeatherObserved entity. Each measurement carries its unit, and the temperature also carries the time it was read:

{
"id": "urn:ngsi-ld:WeatherObserved:65142878",
"type": "WeatherObserved",
"stationName": {
"type": "Property",
"value": "LJUBLJANA/BEZIGRAD"
},
"dateObserved": {
"type": "Property",
"value": "2026-08-21T10:00:00.000Z"
},
"location": {
"type": "GeoProperty",
"value": {
"type": "Point",
"coordinates": [
14.5172,
46.0658
]
}
},
"temperature": {
"type": "Property",
"value": 23.0,
"observedAt": "2026-08-21T10:00:00Z",
"unitCode": "CEL"
},
"relativeHumidity": {
"type": "Property",
"value": 0.8,
"unitCode": "C62"
},
"atmosphericPressure": {
"type": "Property",
"value": 1011,
"unitCode": "A97"
},
"windSpeed": {
"type": "Property",
"value": 0.2,
"unitCode": "MTS"
},
"windDirection": {
"type": "Property",
"value": 54,
"unitCode": "DD"
},
"visibility": {
"type": "Property",
"value": 20,
"unitCode": "KMT"
}
}

The entity passes the WeatherObserved schema. Its required fields are present, humidity is a ratio, and each reading states its unit and, for temperature, its observation time.