Where this page fits
By the time data reaches your integration service, the LoRaWAN network server has already decoded FPort 1 into a JSON object. That shape is defined in Application data contract (keys, frame types, units). This page answers the next question: how to store it durably so dashboards, historians, and alarm rules can query it years later.
Typical sequence:
- Onboard devices on the LNS and install the MOIZ codec.
- Ingest uplink events (MQTT or HTTP) — see runnable examples.
- Persist raw envelopes + normalized metrics (this page).
- Expose to Grafana, SCADA, cloud analytics, etc.
What the LNS gives you vs what you add
| Source | Examples | Your responsibility |
|---|---|---|
| LNS uplink event | devEui, fCnt, fPort, time, rxInfo (RSSI/SNR), object (decoded metrics) |
Store as raw row for audit; deduplicate on (deveui, f_cnt) |
Decoder object |
serial_number, status, pt_1, vib_2_rms_hf, … |
Flatten into normalized time-series rows or JSONB documents |
| Your CMMS / EAM / commissioning | Site, line, asset ID, port wiring, engineering units for pot_* / fourtwenty_* |
Maintain a device registry keyed by DevEUI (and serial) |
The MOIZ decoder does not know your plant hierarchy or asset names. Plan a registry table (or external master data) that you join at query time.
Why not one fixed table per sensor type?
Each Harvestree has four configurable M8 ports. Port types (temperature, vibration, 4–20 mA, etc.) determine which JSON keys appear in each uplink. Configuration can change during the device lifetime (remote config or USB recommissioning). A rigid one column per metric per device schema does not scale across a heterogeneous fleet.
Design goals for most integrator projects:
- Heterogeneous fleet — same tables for all devices; metric names come from decoder keys.
- Traceability — keep raw payload and decoded JSON when the codec or firmware family evolves.
- Queryable analytics — Grafana/SQL over
(time, deveui, metric, value)without redeploying schema on every port change. - Config drift — detect when uplink port bytes no longer match the registry snapshot.
Recommended three-layer model
Separate static identity, immutable receive records, and analytics-friendly metrics. You can implement all three in one database or split raw vs normalized across hot/cold storage later.
| Layer | Table / store | Written when | Purpose |
|---|---|---|---|
| Registry | device_registry |
Commissioning, asset moves, port config changes | DevEUI, serial, site/asset IDs, port config snapshot, firmware family |
| Raw | telemetry_raw |
Every accepted uplink | Full LNS envelope or payload hex + decoded_json, RSSI/SNR — audit, replay, decoder upgrades |
| Normalized | telemetry_normalized |
After validation of object |
Flat time-series for dashboards and alarms (EAV below, or JSONB per frame) |
From decoded JSON to database rows
One uplink produces one raw row and several normalized rows (one per numeric key in object). Respect frame type status:
status = 0(keepalive) — health keys only; no port measurements.status = 1(normal) — health + port keys present in the payload.status = 2(alarm) — same as normal for metrics; also evaluatealarm_source_port/alarm_source_system.
Example input (LNS object, normal frame, PT1000 on port 1):
{
"serial_number": 23118,
"boardTemperature": 23,
"baseTemperature": 30,
"storageVoltage": 3600,
"thermogenVoltage": 49,
"status": 1,
"alarm_source_port": 0,
"alarm_source_system": 0,
"pt_1": 25.0
}
Example normalized rows (same time and deveui for all lines):
time | deveui | metric | value | unit | frame_type
----------------------|------------------|-------------------|--------|------|------------
2026-07-06T08:00:00Z | 70B3D57ED0001234 | serial_number | 23118 | | 1
2026-07-06T08:00:00Z | 70B3D57ED0001234 | boardTemperature | 23 | C | 1
2026-07-06T08:00:00Z | 70B3D57ED0001234 | baseTemperature | 30 | C | 1
2026-07-06T08:00:00Z | 70B3D57ED0001234 | storageVoltage | 3600 | mV | 1
2026-07-06T08:00:00Z | 70B3D57ED0001234 | thermogenVoltage | 49 | mV | 1
2026-07-06T08:00:00Z | 70B3D57ED0001234 | status | 1 | | 1
2026-07-06T08:00:00Z | 70B3D57ED0001234 | alarm_source_port | 0 | | 1
2026-07-06T08:00:00Z | 70B3D57ED0001234 | alarm_source_system | 0 | | 1
2026-07-06T08:00:00Z | 70B3D57ED0001234 | pt_1 | 25.0 | C | 1
The reference consumers implement this flattening in normalize() — see examples/python/ingest_common.py and examples/node/ingest_common.js. Replace their print step with INSERT into telemetry_normalized.
Schema options (before you pick SQL)
| Pattern | Pros | Cons | Typical use |
|---|---|---|---|
EAV — one row per metric (time, deveui, metric, value) |
Same schema for all port types; easy Grafana/Influx-style queries | More rows per uplink; need good indexing on (deveui, metric, time) |
Default recommendation below |
JSONB per frame — store whole object in one column |
Fast to implement; preserves exact decoder output | Harder SQL for single-metric trends across fleet | Prototype, or complement to EAV in telemetry_raw |
| Wide tables — fixed columns per known deployment | Simple queries for one homogeneous site | Schema migration on every port or firmware change | Small single-asset pilots only |
Most projects use raw JSONB + normalized EAV. TimescaleDB hypertables on time are optional but fit irregular LoRaWAN intervals well.
Reference DDL (PostgreSQL / TimescaleDB)
Illustrative schema — adjust names, indexes, retention, and partitioning to your ops standards. This is not a mandatory MOIZ schema; it matches the three-layer model above.
CREATE TABLE device_registry (
deveui TEXT PRIMARY KEY,
serial_number BIGINT,
site_id TEXT,
asset_id TEXT,
port_config JSONB,
firmware_ver TEXT,
commissioned_at TIMESTAMPTZ
);
CREATE TABLE telemetry_raw (
id BIGSERIAL PRIMARY KEY,
time TIMESTAMPTZ NOT NULL,
deveui TEXT NOT NULL,
f_port SMALLINT,
f_cnt BIGINT,
rssi REAL,
snr REAL,
payload_hex TEXT,
decoded_json JSONB
);
CREATE TABLE telemetry_normalized (
time TIMESTAMPTZ NOT NULL,
deveui TEXT NOT NULL,
metric TEXT NOT NULL,
value DOUBLE PRECISION,
unit TEXT,
frame_type SMALLINT,
PRIMARY KEY (time, deveui, metric)
);
-- Optional: SELECT create_hypertable('telemetry_normalized', 'time');
-- Indexes: (deveui, metric, time DESC), (deveui, time DESC) on telemetry_raw
Unit mapping for the unit column
Values in LNS object are already in engineering units — the codec applied wire scale factors. Do not divide JSON fields again before insert.
How to fill telemetry_normalized.unit:
- Fixed decoder keys — temperature (°C), DC mV, vibration LF (mm/s), HF (m/s²), etc. — use the table below or the same rules as
guess_unit()in the ingestion examples. - Commissioning-defined keys —
pot_*,fourtwenty_*, and other float calibration paths — store the unit from yourdevice_registry/ commissioning record, not only from key name. - Dimensionless —
status, alarm flags,vib_*_ratio_*, dry contact — leaveunitempty or use a semantic label (bool,ratio) consistently in your project.
Per-mode key reference: Payload decoding.
| Key prefix / pattern | Unit in decoded JSON |
|---|---|
pt_* | °C |
tck_*, tcj_*, tct_*, tcn_*, tcs_*, tce_*, tcb_*, tcr_* (_cjt, _hjt) | °C |
irA_*, dtempA_* | °C |
weatherA_*_temp | °C |
weatherA_*_humidity | %RH |
weatherA_*_frost | frost index (0 / 1) |
dcdv_*, dcv_* | mV (DC) |
acdv_*, acv_* (not _hz) | mV RMS |
acdv_*_hz, acv_*_hz | Hz |
dcdvEx_*, acdvEx_*, dcmh_*, acmh_*, dccs_*, dcch_*, accs_*, acct_*, acch_* | float32 — calibrated engineering unit (set at commissioning) |
acdvEx_*_hz, acmh_*_hz, accs_*_hz, acct_*_hz, acch_*_hz | Hz |
pot_*, fourtwenty_* | float32 — calibrated engineering unit |
vib_*_rms_lf | mm/s (velocity, LF band) |
vib_*_rms_hf | m/s² (acceleration, HF band) |
vib_*_ratio_* | dimensionless band-energy ratio (0–1) |
dryc_*, async_dryc_* | contact state (0 open / 1 closed) |
boardTemperature, baseTemperature | °C |
storageVoltage, thermogenVoltage | mV |
status, alarm_source_port, alarm_source_system | dimensionless flags |
Backend integration rules
- Use
serial_numberanddeveuias complementary keys; prefer DevEUI for LNS correlation and registry primary key. - Insert into
telemetry_rawbefore or in the same transaction as normalized rows — never drop raw data after normalization. - Store decoded numeric fields as returned by the codec. Re-apply wire factors only if you parse raw bytes yourself (unusual).
- When
status = 0, do not expect port measurement keys — do not synthesize missing port values. - Reject or quarantine frames whose binary length does not match configured port types (see Validation checklist).
- Detect port config changes by comparing uplink header bytes 1–4 with the
port_configsnapshot indevice_registry; refresh registry when operators recommission. - Deduplicate ingested events on
(deveui, f_cnt)— see Ingestion pipelines.