Appearance
Connecting a gateway over MQTT
This page is the integration contract for gateway manufacturers and system integrators. It states what a gateway must publish, where, and in what shape, for the devices it hears to appear in Omaya exactly as if one of Omaya's own gateways had heard them. Everything here is checked against the running listener; where Omaya tolerates variation, the page says so, and where it does not, it says that too.
There is no SDK to embed and nothing to certify: an MQTT client, one topic, JSON.
The one design rule
Omaya does not want the gateway to understand the devices it hears. It wants three things per device: the raw advertisement bytes, unchanged, the signal strength at the gateway, and the gateway's identity. Decoding — Minew, MOKO, Aqara, a customer's own model taught through sensor onboarding — happens in Omaya, per device model.
A gateway that forwards faithfully works with every device Omaya will ever learn. A gateway that pre-parses works with the models it knew on the day its firmware shipped.
Contract summary
| Transport | MQTT 3.1.1 (protocol level 4); MQTT 5.0 also accepted |
| Endpoint | the customer's Omaya host, TLS on 8883 (recommended) or plain TCP on 1883 |
| Authentication | the connector token in the topic; no broker username or password |
| Topic | omaya/{organisation}/{token}/{vendor}/{protocol} |
| Payload | one UTF-8 JSON object per scan window, ≤ 1 MB |
| Required per device | mac, rssi, adv_data |
| Cadence | one message per scan window (1–10 s), heartbeat at least every 60 s when idle |
| QoS / retain | 0 or 1 / never |
| Downlink | none, except Omaya's registerRes to Dusun gateways |
Roles and prerequisites
The connection belongs to the customer's organisation, not to the manufacturer. Nothing is provisioned on Omaya per manufacturer; everything is provisioned per customer, by the customer.
- The customer creates the connector. In Omaya: Connectors → IoT Gateway (MQTT). They choose a vendor format (
genericfor a new manufacturer, or one of the native formats below), a protocol (ble,wifi,lora, ortelemetryfor already-decoded readings), a name, and save. Omaya generates a connector token and displays the complete topic, the broker host and port, with a copy button. That is the entire hand-over to you: host, port, topic. - The customer registers each gateway under Device → Gateway: its MAC address, the venue and floor it serves, its position on the plan. A message from a MAC that is not registered is noted but not processed (see pipeline).
- The customer registers each device under Device → Sensor with its model. A device that is not registered is dropped at step 7 of the pipeline unless auto-discovery or the onboarding wizard is running.
Your gateway therefore needs exactly three configuration items: host, port, topic. Design the configuration UI or provisioning file around those three strings and nothing else is required.
Transport
Endpoints
| Port | Transport | Use |
|---|---|---|
| 8883 | MQTT over TLS | Recommended for every new integration and for any gateway on a network you do not control |
| 1883 | MQTT over plain TCP | Kept for existing fleets and for gateways that cannot do TLS; traffic and topic are readable on the wire, so the token is exposed to anyone on the path |
Both listeners are the same broker; a customer can run gateways on both at once. The host is the same host name as the customer's Omaya web address.
TLS on 8883
| Versions | TLS 1.3 and TLS 1.2. TLS 1.1 and below are refused |
| Server certificate | a publicly trusted certificate for the instance host name (Let's Encrypt on the standard deployment; chain to ISRG Root X1) |
| Client certificate | not requested. Mutual TLS is not used |
| Host name check | the certificate matches the instance host name; connect by name, not by IP, and send SNI |
| Certificate renewal | every 60–90 days, automatically. The certificate the gateway sees will change. Validate against the CA store; do not pin the leaf certificate or its fingerprint |
| CA store on the gateway | must contain the public roots (Mozilla's bundle, or at least ISRG Root X1). Ship it in firmware and keep a way to update it |
| Clock | certificate validation needs a roughly correct clock on the gateway (within a few hours). A gateway that boots at 1970 must obtain time (NTP, or from the cellular network) before its first TLS connect, or TLS fails until it does |
| Ciphers | the broker prefers its own order; any modern AEAD suite is accepted (TLS_AES_256_GCM_SHA384, TLS_AES_128_GCM_SHA256, TLS_CHACHA20_POLY1305_SHA256, and the ECDHE-RSA-AES-GCM suites for 1.2) |
A quick external check: a TLS client connecting to the host on 8883 with SNI set should complete a TLS 1.3 handshake and receive a certificate whose subject is the instance host name and whose chain validates with no warnings. If the gateway's TLS library reports "unknown CA", the root bundle on the gateway is missing or stale; if it reports a name mismatch, the gateway connected by IP or without SNI.
MQTT connection parameters
| Parameter | Value | Notes |
|---|---|---|
| Protocol | MQTT 3.1.1 | MQTT 5.0 is accepted; 3.1 is accepted |
| Client identifier | unique per gateway, 1–23 characters safe, up to 256 accepted | The gateway MAC without separators is the conventional choice. Two connections with the same client id evict each other — a fleet that shares one id will reconnect in a loop |
| Username / password | empty | The broker does not authenticate; the topic token does. If a particular instance is configured to require credentials, the customer's connector page shows them |
| Clean session | true | There is nothing for the broker to persist for a publisher |
| Keep-alive | 30–60 s | The broker allows 1.5 × keep-alive of silence before closing the session. Send PINGREQ on schedule when idle |
| Will message | not needed | Omaya derives gateway online state from the messages it receives, not from a will |
| QoS | 0 or 1 | Omaya subscribes at QoS 0. Publishing at QoS 1 gives you the PUBACK as delivery confirmation to the broker; it does not change what Omaya does. QoS 2 works but buys nothing |
| Retain | false, always | A retained scan is re-delivered to the listener on every reconnect and re-processed as new presence |
| Maximum packet | 1 MB | Larger publishes are rejected by the broker and the session is closed |
| Connections | one, persistent, per gateway | Connect-publish-disconnect per message is the most common cause of a gateway that "works in the lab and not on site" |
Reconnecting and buffering
Reconnect with exponential backoff (1 s → 60 s cap, with jitter). Do not resend the session's earlier messages after a reconnect.
Buffer while disconnected only if the buffer is bounded and every buffered message carries its own timestamp. A gateway that stores a night of scans and replays them at 08:00 without timestamps produces a night of presence at 08:00. With timestamps, Omaya records each packet — presence, trail, readings — at its capture time, up to one hour back; only live positioning ignores anything older than 30 s (see time handling). Older than an hour and the packet is treated as having a wrong clock. The safe default for most products is: drop what could not be sent, carry on.
The topic
omaya/{organisation}/{token}/{vendor}/{protocol}| Segment | Format | Where it comes from |
|---|---|---|
omaya | literal | fixed |
organisation | 1–50 characters: lowercase letters, digits, hyphens | the customer's organisation id in Omaya — what they type at the login form. Not the company's display name |
token | exactly 16 lowercase hexadecimal characters (64 bits) | generated when the customer saved the connector; shown on the connector page |
vendor | one of generic, dusun, minew, chirpstack, aruba, mikrotik, kiwire | the payload format the connector was created with |
protocol | one of ble, wifi, lora, telemetry | the radio the connector was created with, or telemetry for already-decoded readings |
Rules:
- Exactly five levels. No leading slash, no trailing slash, no extra levels.
omaya/acme/…/generic/ble/gw01is malformed and dropped. - The token, organisation and vendor are checked together. The token was issued to one organisation for one vendor format. A message whose topic pairs it with any other organisation or vendor is dropped. The protocol segment is carried through as-is.
- Dropped means silent. The broker accepts the publish (you get PUBACK at QoS 1). Omaya never reads it, writes nothing, logs nothing. This is deliberate — it makes token guessing unobservable — but it means a mis-typed topic produces no error anywhere. The verification procedure is how you find it.
- Case matters. Publish the topic exactly as the connector page shows it.
- One topic, many gateways. Every gateway a customer owns from you publishes to the same topic. The gateway identifies itself inside the payload (
gateway_mac). Omaya subscribes to a wildcard, so nothing is provisioned per gateway. - Token rotation. The customer can rotate the token from the connector page. The old topic stops being read immediately; gateways must be given the new topic. Design for the topic to be changeable in the field without a firmware update.
- Token cache. The listener caches a token's validity for ten minutes. A newly created or newly rotated token is live immediately (the save primes the cache); a connector that is disabled stops being read immediately too.
The payload — generic format
Use the generic format unless your product already speaks one of the native formats.
Shape
One JSON object per scan window: the interval the gateway listens before publishing everything it heard. One to two seconds is typical for BLE; up to ten is fine. Everything heard in the window goes in one message.
json
{
"gateway_mac": "AA:BB:CC:DD:EE:01",
"timestamp": 1789614865,
"devices": [
{
"mac": "C3:00:00:4C:CA:3F",
"rssi": -54,
"adv_data": "0201061BFF3906CA003FCA4C0000C364600D40000000500000040061D865FE",
"dev_name": "",
"addr_type": 0
},
{
"mac": "C3:00:00:38:60:4C",
"rssi": -58,
"adv_data": "0201060303E1FF1216E1FFA1034B000CFFF9FEFB4C60380000C3",
"dev_name": "",
"addr_type": 0
},
{
"mac": "71:DD:9E:30:10:E6",
"rssi": -66,
"adv_data": "02011A0AFF4C0010057C1CED6729020A00",
"dev_name": "",
"addr_type": 1
}
]
}Both device frames above are real captures — a Minew MSR01-B people counter and a Minew MWC01 tag — and decode in Omaya as-is. The third is a phone with a random address, which Omaya sees and ignores.
Encoding rules
- UTF-8, no byte-order mark, a single top-level object. Arrays at the top level are read only for the Minew format.
- Numbers as JSON numbers, not strings:
"rssi": -54, not"rssi": "-54". (Strings are coerced, but do not rely on it.) - Keys are case-sensitive and lowercase as written here.
- Unknown keys are kept with the packet and ignored. You may add your own fields (firmware version, channel, temperature of the gateway) without breaking anything.
- Nulls are treated as absent.
Top level
| Field | Type | Required | Rules |
|---|---|---|---|
gateway_mac | string | yes | The publishing gateway's own MAC. Accepted spellings: AA:BB:CC:DD:EE:01, aa-bb-cc-dd-ee-01, AABBCCDDEE01, aabbccddee01 — all normalised to the same value. Also accepted under the keys gw_mac or ap_mac, or nested as device_info.mac (MOKO's envelope) or gateway.mac. Must equal the MAC registered under Device → Gateway. A message without it is not attributable and is dropped. |
timestamp | integer or string | no | Capture time of the window. Unix seconds (integer), or an ISO 8601 string with an offset (2026-09-17T11:14:25+08:00). Applies to every device in the message; a device row may carry its own timestamp instead when the envelope has none. Not milliseconds: a millisecond value is read as seconds, lands 50,000 years out, and is discarded as a clock fault (the packet is then stamped with its arrival time and the gateway is flagged as drifting). If the gateway has no trustworthy clock, omit the field. |
devices | array | yes | The rows below. An empty array is a heartbeat. Also accepted under the key data. |
Each device
| Field | Type | Required | Rules |
|---|---|---|---|
mac | string | yes | The address as heard on air, any of the spellings above. Also accepted as device_mac or client_mac. A row without one is skipped; the rest of the message is still processed. |
rssi | integer | yes | Received signal strength at this gateway in dBm. Valid range −120 to −1. Anything outside (0, positive numbers, −127 sentinel values) is stored as no reading and the packet is excluded from positioning. Send the per-packet reading; do not average across the window. Also accepted as signal. |
adv_data | string | yes for anything to be decoded | The BLE advertising PDU payload exactly as received, as hexadecimal. See the advertisement bytes — this field is the whole point. Without it a device is tracked for presence and position only. Also accepted as raw_data, rawData or raw; or split as adv (advertisement) + rsp (scan response), which Omaya joins in that order. |
protocol | string | no | ble (default), wifi, or lora, if one message mixes radios. Normally omitted; the topic already says. |
packet_type | string | no | scan (default), sensor, heartbeat. Omaya determines this from the bytes; set it only if you know better. |
dev_name | string | no | The Local Name from the advertisement (AD types 0x08/0x09) if your radio stack already extracted it; up to 120 characters. Used as a hint when the customer adopts a discovered device. |
addr_type | integer | no | 0 public, 1 random. Stored with the packet; not interpreted today. |
channel | integer | no | 37, 38 or 39. Stored; not interpreted. |
The advertisement bytes (adv_data)
adv_data is the payload of the advertising PDU — the AdvData field of ADV_IND, ADV_NONCONN_IND, ADV_SCAN_IND or ADV_DIRECT_IND — from its first byte to its last, as received. Concretely: the sequence of AD structures, each [length][AD type][data…], up to 31 bytes for legacy advertising.
- Hexadecimal, two characters per byte, no separators, no
0xprefix, either case.0201061BFF…, not02 01 06 1B FF …and not0x02,0x01…. - Include everything. The Flags structure (
02 01 06), the service UUIDs, the manufacturer data — all of it, in the order received. Decoders locate fields by walking the AD structures; a frame with the first structure stripped is a frame at the wrong offset. - Scan responses go after the advertisement. If your radio performs active scanning, append the
SCAN_RSPpayload to the end ofadv_data, or send it separately asrspnext toadvand Omaya will join them. Do not send it on its own and do not put it first: decoders walk the AD structures from the front and expect the advertisement there. Some devices put a reading only in the scan response — MOKO's button range carries battery and accelerometer there — so dropping it loses that reading. (Earlier versions of this page said to drop scan responses; that was true until 2026-09.) - Do not deduplicate identical frames within a window unless you also send a count; a device advertising ten times a second is telling Omaya it is present ten times a second.
- Length: 62 hex characters for a full legacy advertisement; up to 508 for an extended one. Omaya reads up to 1024 characters.
Read a real one. The first device above is an MSR01-B people counter:
02 01 06 AD structure 1: length 2, type 0x01 Flags, value 0x06
1B FF 39 06 CA 00 3F CA 4C 00 AD structure 2: length 27, type 0xFF Manufacturer Specific Data,
00 C3 64 60 0D 40 00 00 00 company id 0x0639 (Minew), then the model's own frame:
50 00 00 04 00 61 D8 65 FE frame type, the device's MAC repeated, battery, the count fieldsOmaya's MSR01-B decoder reads the count from fixed offsets inside that second structure. It finds the structure by walking from the first byte; if a gateway delivered only 3906CA003F… (manufacturer data without its length and type byte) or only CA003F… (without the company id), the walk fails and the frame is a presence ping and nothing more. The test is simple: put a phone BLE scanner (nRF Connect shows raw bytes) next to the device and compare its hex with yours. They should be identical.
Already-decoded readings (values)
Some gateways do not relay radio frames at all: an edge gateway that polls power meters, PDUs, UPS units, cooling units or generators over Modbus TCP, OPC UA or SNMP already knows the value is 412.8 kW. Such a gateway sends the values themselves. A device entry carries a values object (telemetry and metrics are accepted spellings) instead of rssi and adv_data:
json
{
"gateway_mac": "DC:ED:6E:00:00:01",
"timestamp": 1789614900,
"devices": [
{ "mac": "DC:01:01:00:00:01", "values": { "kw": 412.8, "kwh": 1203455.2, "voltage": 415.2 } },
{ "mac": "DC:01:02:00:00:02", "values": { "load_pct": 41, "battery_pct": 100, "on_battery": false } },
{ "mac": "DC:01:03:00:00:03", "values": { "supply_temp_c": 17.4, "return_temp_c": 28.9, "running": true } },
{ "mac": "DC:01:04:00:00:04", "values": { "temperature": 23.6, "humidity": 46 } }
]
}- Identity. Each device still needs its own identifier in MAC form (six pairs of letters or digits) or a 16-character EUI, registered in Omaya like any other device. A Modbus unit has no MAC: assign one per device in the gateway and keep it stable.
- Values are numbers,
true/false, or state words (on/off,running/stopped,open/closed), which become 1 and 0. Names are lower-cased, with spaces and dashes becoming underscores; anything else — a firmware string, a nested list — is ignored. At most 64 values per device. One level of grouping is flattened:{"ups": {"load_pct": 41}}is read asload_pct. - What Omaya files where. A few names are recognised by exact spelling and land in the platform's own tables: energy (
kwh,energy_kwh,total_kwh, orwh/energy_whin watt-hours), power (kw,power_kw,active_power_kw,load_kw, orwatts/power_w),voltage,current,temperatureandhumidity. Every value, those included, is also kept under its own name, so a rule can watch it and a custom report can chart it. A cooling unit'ssupply_temp_cis not taken as the room's temperature, andvoltagehere is mains voltage, never a battery. - Cadence. Every 10–60 seconds is plenty; there is no RSSI to average. Mixing radio devices and value devices in one message is allowed.
- Topic. The same topic scheme. Create the gateway's connection with protocol Telemetry so the customer's integration page says what it is.
Omaya's Data Centre app reads these values for PUE, rack heat, UPS and cooling state; see its Setup page for the names it understands per kind of equipment.
Heartbeat
A gateway that hears nothing should still say it is alive:
json
{ "gateway_mac": "AA:BB:CC:DD:EE:01", "timestamp": 1789614900, "devices": [] }This updates the gateway's last seen and nothing else. Omaya's gateway state is derived from last seen: online within the last 5 minutes, stale between 5 and 15 (still counted as up), offline after 15, at which point the customer's administrators are notified. Send a heartbeat at least once a minute when idle; once every 15–30 s is what Omaya's own gateways do. When there is traffic, every scan window is a heartbeat, so no separate message is needed.
One gateway or many radio heads
gateway_mac is the identity Omaya attributes hearing to. A device is placed in the zone of the gateway that hears it loudest, and positioned by comparing RSSI across gateways. So:
- A single-radio gateway publishes under its own MAC.
- A hub with several radio heads publishes one message per head, each under that head's MAC, each head registered in Omaya at its own position. Merging heads under the hub's MAC merges their rooms.
Size and cadence
| Typical | Limit | |
|---|---|---|
| Devices per message | 10–60 | no fixed limit; 1 MB total |
| Bytes per device row | 120–200 | — |
| Message size | 2–10 KB | 1 MB |
| Messages per gateway | one per scan window, 0.5–1 /s | — |
| Scan window | 1–2 s BLE, up to 10 s LoRa | — |
Publish per window, never per device. A busy lobby heard by one gateway is hundreds of devices a second; as one message a second it is nothing, as hundreds of messages a second it is a problem for the broker, for your radio link, and for your power budget.
Time and clocks
- If
timestampis present, Omaya compares it with its own clock on arrival and records the difference as the gateway's clock skew. It is not shown on the customer's screens; Omaya's operators can report it per gateway, and will, during an integration test. - Positioning uses only packets captured within 30 seconds of arrival. A gateway whose clock is more than 30 s out keeps heartbeating, keeps showing as online, keeps its devices seen — and silently contributes nothing to trilateration, so a floor drops from positions to "near gateway X". This is the quiet failure to design against: NTP on the gateway, or no
timestampat all. - A packet whose timestamp is more than one hour from arrival (either direction) is stamped with the arrival time instead and the gateway is marked as drifting; readings continue.
- If
timestampis absent, the arrival time is used, no skew is recorded, and positioning works normally. This is the correct choice for a gateway without a reliable clock — and it is why the field is optional. - Omaya's own clock is NTP-disciplined UTC. Send UTC seconds; send ISO strings with an explicit offset; never send local time without an offset.
What Omaya does with a message
In order, and every step is a place a message can stop:
- Topic check. Five levels; organisation, token and vendor must match a connector that is enabled. Otherwise dropped, silently.
- Parse. The body must be valid JSON and an object (or, for
minew, an array). Otherwise dropped, silently. - Expand. One packet per device row: organisation, gateway, device, RSSI, protocol, packet type, capture time, the raw row. Rows without a device MAC are skipped. An empty list yields one heartbeat packet.
- Gateway check.
gateway_macmust be a registered gateway of that organisation. If not: the MAC, the protocol, a packet count and the last payload are recorded in the organisation's discovered gateways list for the administrator to register, and the packet stops here. Registered: the gateway's last seen is updated (this is what keeps it online) and the clock skew is recorded. - Heartbeat? A heartbeat packet stops here, having done its job.
- RSSI floor. Each gateway can carry a minimum RSSI set by the customer (Device → Gateway). Packets below it are dropped. Default: no floor.
- Device check. The device MAC must be registered with a model. If not: dropped — unless the customer's auto-discovery is on (the MAC is listed for adoption) or the sensor-onboarding wizard is running (every packet is shown live). Registered: the device's last seen, RSSI and hearing gateway are updated; the raw packet is stored.
- Presence and position. Live keys for the device's zone and the entity it is bound to; the device trail and entity trail rows; zone auto-assignment; the positioning engine when more than one gateway heard it. All within seconds — this is what Right Now, the floor plan, Roll Call and the rules read.
- Decode. The decoder for the device's model reads
adv_dataand produces the model's fields — temperature, humidity, battery, button, motion, people count, occupancy. A model with no decoder yields nothing here and the device remains a presence-only tag. - Report routing. Decoded fields go to the report tables — device status, battery, temperature, people count, enter/exit, and the module-specific tables — as hourly rows; presence and trail rows are written as they happen.
- Rules. Live rule evaluation on the decoded fields and on presence events.
Two caches matter for testing: a token's validity and a gateway's registration are each cached for ten minutes on the listener after the first lookup. Creating or rotating a token primes the cache immediately; registering a previously unknown gateway may take up to ten minutes to take effect for packets that were already being dropped.
Native formats Omaya already reads
If your product already has a JSON format in the field, check these before writing a translation layer. The customer picks the matching vendor when they create the connector.
Dusun (dusun)
The Dusun DSGW family's format, as captured live. Two gateways publishing to one topic; the gateway is data.mac:
json
{
"from": "BLE", "to": "GATEWAY", "type": "reportAttribute",
"mac": "30:ae:7b:ea:ba:ed", "deviceCode": "", "time": 1789614865,
"data": {
"attribute": "mod.device_list",
"mac": "30:ae:7b:ea:ba:ed",
"value": {
"device_list": [
{
"ble_addr": "C3:00:00:4C:CA:3D", "addr_type": 1, "scan_rssi": -49,
"data": "0201061BFF3906CA003DCA4C0000C364600D4000000050000004002CE15A1A",
"dev_name": "", "modelstr": "Bledevice", "connectable": 0,
"connect": 0, "scan_time": 1789614864
}
]
}
}
}Field mapping: data.mac → gateway, ble_addr (or mac) → device, scan_rssi (or rssi) → RSSI, data (or adv_data, raw_data) → adv_data, time → timestamp.
Dusun gateways also publish {"type":"registerReq","mac":"…"} every few seconds until answered. Omaya answers on the gateway's own command topic t/{gateway-mac} (lower-case MAC) with {"to":"GATEWAY","from":"CLOUD","type":"registerRes","mac":"…","data":{"res":0}}. That is the only message Omaya ever publishes towards a gateway. The cmdResult messages the gateway sends back are ignored.
Minew (minew)
The Minew G1 family posts a JSON array, one element per heard device, plus one element per gateway heartbeat:
json
[
{ "type": "Gateway", "mac": "AC233FA00001", "gatewayFree": 79, "gatewayLoad": 0.21, "timestamp": "2026-09-17T03:14:25Z" },
{ "type": "S1", "mac": "AC233FA1B2C3", "gw_mac": "AC233FA00001", "rssi": -63, "rawData": "0201060303E1FF…", "timestamp": "2026-09-17T03:14:25Z" }
]mac → device, gw_mac → gateway (or the top-level gw_mac), rssi, rawData → adv_data, timestamp; type: "Gateway" elements are heartbeats.
ChirpStack (chirpstack, protocol lora)
For LoRaWAN, Omaya does not talk to gateways — it talks to the network server. ChirpStack decrypts each uplink, runs the device's codec and hands Omaya the result through one of its integrations. Why the network server, and not the gateway, is the boundary is explained under adding an unknown sensor.
Which route. Omaya reads ChirpStack v4, and the ChirpStack v3 Application Server, over either integration:
| Route | Set where | Use it when |
|---|---|---|
| HTTP integration (recommended) | per application, in ChirpStack's web interface | always works: nothing to change on ChirpStack's server, and one application's devices go to one Omaya organisation |
| MQTT integration | once per ChirpStack server, in its configuration file | ChirpStack serves only this customer and nothing else reads its MQTT events — the v4 MQTT integration has a single broker and a single topic for every application |
Omaya reads only its own five-level topic. Pointing Omaya at ChirpStack's own broker, or bridging ChirpStack's application/… topic tree across unchanged, does not work.
The payload must be JSON. ChirpStack can also send Protobuf, which Omaya cannot read: an HTTP post is refused with 422, an MQTT message is dropped without a trace.
In Omaya
- Create the connector. Connectors → IoT Gateway (HTTP) (or IoT Gateway (MQTT)) → add. Vendor ChirpStack (LoRa), protocol LoRaWAN (LoRa / LoRaWAN on the MQTT page). Copy the URL (or the topic) it shows.
- Register every gateway under Device → Gateway: the Gateway ID exactly as ChirpStack's Gateways page shows it — 16 hexadecimal characters, no colons or dashes — and the zone it serves. An uplink heard only by unregistered gateways is recorded for Omaya support but goes no further; there is no list of such gateways in the admin yet, so register them from ChirpStack's list rather than waiting for them to appear.
- Register every device under Device → Sensor: its DevEUI (16 hexadecimal characters, no separators), the zone its readings belong to, and its model.
- Give the device a codec in ChirpStack (the device profile's Codec tab). Omaya reads the codec's output, so a device without one is seen but reports nothing. Omaya recognises the common field names — for example
temperature,humidity,co2,tvoc,pm2_5,pressure,illumination,battery,people_count,occupancy— including the spellings Milesight, Dragino and Elsys codecs use. ChirpStack's built-in Cayenne LPP codec is read too — temperature, humidity, illuminance, barometer, presence and GPS; where a device reports the same measurement on several channels, the lowest channel is the one recorded. When there is no codec, Device → New sensor types can decode the raw bytes instead.
In ChirpStack v4 — HTTP integration
Applications → the application → Integrations → HTTP → add:
| Setting | Value |
|---|---|
| Payload encoding | JSON |
| Event endpoint URL(s) | the connector URL, https://{host}/api/ingest/{token} |
| Headers | none needed — the token in the URL is the credential |
ChirpStack adds ?event=up, ?event=status and so on to the URL itself; leave that to it. Repeat for each application whose devices belong in this organisation.
In ChirpStack v4 — MQTT integration
In ChirpStack's configuration file (chirpstack.toml), under the global integration settings. Restart ChirpStack afterwards.
toml
[integration]
enabled = ["mqtt"]
[integration.mqtt]
# Omaya's broker; ssl://{host}:8883/ for TLS.
server = "tcp://{host}:1883/"
username = ""
password = ""
json = true
# The connector topic, exactly — no template variables.
event_topic = "omaya/{organisation}/{token}/chirpstack/lora"
# ChirpStack listens here for downlinks: Omaya sends one only when someone
# changes a sensor's setting on its Configure page. Keep the
# token in it so nobody else on the broker can reach your devices.
command_topic = "omaya-downlink/{token}/{{application_id}}/{{dev_eui}}/{{command}}"Every event from every application now goes to the one topic, and Omaya tells them apart by their contents. ChirpStack's default event_topic (application/{{application_id}}/device/{{dev_eui}}/event/{{event}}) will not reach Omaya.
ChirpStack v3 (Application Server)
The same connector reads v3's event shape (devEUI, rxInfo[].gatewayID, loRaSNR) with either JSON marshaler — JSON (v3) or JSON, not Protobuf. For the HTTP integration, set the connector URL as the uplink and device-status URL (or as the single event endpoint on 3.10 and later); for MQTT, set marshaler, server and the event topic in the [application_server.integration] section as above. The network server built into Milesight's UG56 and UG65 gateways also sends this shape; its set-up, including the codec wrapper it needs, is under Milesight UG56 and UG65.
What Omaya does with each event
| Event | Omaya |
|---|---|
up — an uplink | One reception per gateway that heard it (its RSSI and SNR kept), so the device's last seen, gateway and zone update; one reading from the codec output (object), however many gateways heard it; the raw bytes (data) kept for New sensor types and custom decoders. Time: the event's time, else the gateway's receive time |
status — battery and link margin | The battery level is recorded as the device's battery reading. Nothing when the device is mains-powered or cannot measure. ChirpStack only sends this when the device profile's device-status request frequency is above zero |
join, ack, txack, log, location, integration | No reading. On v3, join and ack carry the gateways that heard them, so they count as the device being heard (its last seen moves); v4 sends them without, and they are ignored |
| Gateway statistics and connection state | Never sent by an integration — ChirpStack keeps them on its own broker. A LoRaWAN gateway is therefore online while it relays uplinks: stale after 30 minutes without one, offline after 60 |
The uplink Omaya reads, trimmed to the fields it uses:
json
{
"deduplicationId": "3ac7e3c4-4401-4b8d-9386-a5c902f9202d",
"time": "2026-09-24T03:14:25.123456789+00:00",
"deviceInfo": { "devEui": "70b3d57ed0061234", "deviceName": "th-01" },
"fCnt": 118,
"fPort": 2,
"data": "AQIDBA==",
"object": { "temperature": 24.5, "humidity": 61, "battery": 92 },
"rxInfo": [ { "gatewayId": "a84041fdfe2a1b2c", "rssi": -97, "snr": 7.5 } ],
"txInfo": { "frequency": 923200000, "modulation": { "lora": { "spreadingFactor": 9, "bandwidth": 125000 } } }
}Checking it
Send one uplink (or wait for the next), then:
| Where | Pass |
|---|---|
| ChirpStack: the device's Events tab | an up event, with a decoded object |
| Omaya: Device → Gateway | the gateway's last seen moves |
| Omaya: Device → Sensor | the device's last seen and gateway update |
| Omaya: the device's page, or its report | the codec's readings appear, not only seen |
When it does not work
| Symptom | Cause | Fix |
|---|---|---|
ChirpStack logs 422 from the HTTP integration | Payload encoding is Protobuf | Set it to JSON |
ChirpStack logs 401 or 403 | Wrong token, or the connector is disabled | Copy the URL from the connector again; check enabled |
| MQTT: nothing at all arrives | event_topic is not exactly the connector topic, json is off, or ChirpStack is still publishing to its own broker | Compare the topic segment by segment; set json = true; check server |
| Gateway never online, devices never seen | The gateway ID is not registered, or was typed with separators | Register it as 16 hex characters |
| Gateway online, device never seen | The DevEUI is not registered, or has separators | Register it as 16 hex characters |
| Device seen, no readings | The device profile has no codec, or the codec's field names are not ones Omaya knows | Add the codec; otherwise decode it with New sensor types |
| No battery | The codec does not report one and ChirpStack never asks the device | Set the device profile's device-status request frequency |
| Gateway flips between online and offline | Its devices send less often than every 30 minutes | Expected for a quiet gateway; it is healthy if its devices report |
| Readings filed in the wrong hour | ChirpStack's clock is more than an hour off, so Omaya uses the arrival time | Set up NTP on the ChirpStack server |
MOKO MKGW series (generic)
MOKO's Wi-Fi and cellular gateways (MKGW-mini, MK107, MKGW1/3…) publish one JSON object per scan window. It is read by the generic parser, so the customer picks vendor Generic when creating the connector. As captured from MOKO's SDK (MsgNotify, msg_id 3004 = BLE scan result):
json
{
"msg_id": 3004,
"device_info": { "device_id": "MKGW-mini01_2A6F", "mac": "D0:CF:5E:2A:6F:10" },
"data": [
{ "timestamp": 1789614865, "type": "iBeacon", "mac": "C30000175320", "rssi": -61, "raw_data": "0201061AFF4C000215…" }
]
}device_info.mac → gateway, mac → device, rssi, raw_data → adv_data (or adv + rsp when the gateway is set to upload them separately), each row's timestamp. Other msg_id values (OTA, reset, networking status, config acknowledgements) carry no data rows and are read as heartbeats.
In the MKScannerPro app, set the gateway's publish topic to the connector topic and leave the subscribe topic as any non-empty string of your choosing — Omaya never publishes towards these gateways. Under Upload data option turn on raw data (or adv and rsp) and timestamp; leave the RSSI and duplicate-data filters off, because positioning needs every frame. Step by step on the gateways page.
Trackers that report themselves (generic)
A cellular badge or tracker with its own SIM is not a gateway: it publishes its own readings rather than tags it heard. It is registered as a device (Device → Sensor), never as a gateway, and set to send JSON.
MOKO PS101-M. Omaya reads the badge's own protocol (reports msg_id 3100–3109 and the answers to its commands): position, Bluetooth and Wi-Fi scans, battery and charging, SOS / Alert / man-down start and end, low power, switch-off, stored reports with their own time. Its MAC may be in a top-level mac or in device_info.mac. Setup, and what Omaya does with each report: MOKO PS101-M.
Any other tracker is read when the envelope names it and data is an object (not a list) that carries a position:
json
{
"msg_id": 3020,
"device_info": { "device_id": "TRK_2A6F", "mac": "D0:CF:5E:2A:6F:20" },
"data": { "timestamp": 1791100000, "gps": { "lat": 3.1579, "lon": 101.7116 }, "accuracy": 8 }
}The shape is what Omaya accepts, not a manufacturer's documented format. The position may sit at the top of data or inside a gps, location or position object, as latitude/lat and longitude/lon/lng, and may carry its unit ("3.1579°"). 0, 0 means no fix yet and is ignored. For such a tracker only the position is read; a platform that forwards positions in its own shape can use GPS positions from another platform.
- Positions go to the GPS trail: geofence visits, trips and stops — see Indoor and outdoor. The tracker is kept out of gateway room positioning.
- A gateway's own status messages in the same envelope are unaffected.
Aruba, Mikrotik, Kiwire
Wi-Fi association and BLE-over-AP formats from those products, for customers who already run them. Not a target for a new gateway product.
Publishing over HTTPS instead
The same payload can be POSTed. The customer creates an IoT Gateway (HTTP) connector, which yields a token-authenticated URL:
POST https://{host}/api/ingest/{token}
Content-Type: application/json202 Accepted when queued; 401 unknown token; 403 connector disabled; 422 empty or unparseable body, or more than 500 messages in one request.
One request carries one message or a JSON array of up to 500. Each gateway may send up to 300 messages a minute. Omaya recognises the gateway by an X-Gateway-Mac header or by the gateway's MAC or id inside the message, and failing both by the sender's IP address — so gateways behind one address that send neither share a single allowance. Messages over the limit are dropped rather than refused: the call still answers 202, with accepted lower than total. Compare the two. GET /api/ingest/{token}/ping returns 200 with the connector's vendor and protocol, which is a convenient first test for a gateway that can do HTTPS but not MQTT. Everything from step 2 of the pipeline onward is identical. Prefer MQTT for anything publishing more than a few times a minute.
Verifying an integration
Do this with the customer's Omaya open. Each step names where its effect is visible, so a failure is located rather than guessed at.
| # | Step | Where to look | Pass |
|---|---|---|---|
| 1 | TLS handshake to the host on 8883 with SNI | your TLS client | TLS 1.2/1.3 established; certificate subject is the host name; chain validates |
| 2 | MQTT CONNECT with your client id, keep-alive 60, clean session | your MQTT client | CONNACK return code 0 |
| 3 | Publish a heartbeat ("devices": []) to the topic, QoS 1 | your client · Device → Gateway | PUBACK received · within 10 s the gateway's last seen moves (registered) or the MAC appears under discovered gateways (not yet registered) |
| 4 | Nothing moved in step 3 | — | The topic is wrong. Compare all five segments, character by character, with the connector page. This is the failure with no error message |
| 5 | Register the gateway, publish a scan window with a registered device | Device → Sensor | The device's last seen, RSSI and gateway update |
| 6 | The device is a known model | the device's page | A decoded reading appears (temperature, count, battery…), not only seen |
| 7 | Device in a zone | Right Now | The device is counted in the zone its gateway serves |
| 8 | Ten minutes of traffic | Reports → Device Trail | Rows for the device with the gateway named |
| 9 | Stop publishing | Device → Gateway | stale after 5 minutes, offline after 15, and the administrator is notified |
| 10 | Publish for a minute with timestamp set from the gateway's clock | Omaya support runs the gateway clock report | Skew under a few seconds. Over 30 s and the gateway is excluded from positioning without any visible symptom |
Then send Omaya's support a thirty-second capture of what your gateway published on a real site — the raw messages, unedited — and we will confirm every device in it decodes.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Connected, PUBACK received, nothing changes in Omaya | Topic does not match the connector (token, organisation id or vendor differ), or the connector is disabled | Copy the topic from the connector page; check enabled |
| Gateway is discovered but never online | The gateway MAC is not registered, or was registered under a different spelling of a different MAC | Register the MAC exactly as your gateway sends it; wait up to ten minutes |
| Gateway online, devices never seen | Device rows lack mac, or MACs are rewritten, or the devices are not registered | Send the on-air address; register the devices or turn on auto-discovery |
| Devices seen, no readings | adv_data missing, truncated, or not the full AD-structure sequence | Compare with a phone scanner's raw bytes; send the full advertisement |
| Readings wrong by a constant | Wrong model chosen on the device, not a gateway problem | Change the model under Device → Sensor |
| Gateway flips between online and offline | Heartbeats slower than every 5 minutes, or connection dropped and re-established per message | Heartbeat every ≤ 60 s; one persistent connection |
| Devices are seen but never positioned — the floor plan shows them at the gateway, not in rooms | timestamp more than 30 s off: milliseconds, local time without an offset, or no NTP | Send Unix seconds or ISO 8601 with offset from an NTP-set clock; or omit the field |
| Presence appears in bursts at the top of the hour | The gateway batches and replays without timestamps | Publish per window; if you must buffer, timestamp every message |
| Everything works over 1883, TLS fails | Missing root CA on the gateway, connecting by IP, no SNI, or gateway clock at 1970 | Ship the Mozilla root bundle; connect by host name with SNI; get NTP before the first connect |
| Broker closes the connection on publish | Message over 1 MB, or a malformed packet | Split the window; check the client's MQTT encoding |
| Two gateways keep disconnecting each other | Same client id | Client id = gateway MAC |
Checklist for a manufacturer
Before a product is described as supporting Omaya:
- [ ] Host, port and topic are configurable in the field without a firmware update
- [ ] TLS on 8883: TLS 1.2+, public root CA bundle on the device, connects by host name with SNI, validates the chain, does not pin the leaf certificate
- [ ] Time from NTP or the network before the first TLS connect
- [ ] One persistent MQTT connection per gateway, client id unique per unit, keep-alive ≤ 60 s, exponential-backoff reconnect
- [ ] One message per scan window; all heard devices in
devices[] - [ ]
gateway_mac; per devicemac,rssi(−120…−1),adv_data - [ ]
adv_datais the complete advertisement, hex, as received — verified against a phone scanner - [ ]
timestampis Unix seconds from a real clock, or omitted - [ ] Empty-list heartbeat at least every 60 s when idle
- [ ] QoS 0 or 1; retain never; messages under 1 MB
- [ ] No RSSI filtering above −95 dBm; no cross-window deduplication; no MAC rewriting
- [ ] Multi-radio hubs publish one message per radio head under that head's MAC
- [ ] A 30-second raw capture from a field unit has been sent to Omaya and confirmed
Meet those and there is nothing else to certify.
Compatibility
This contract is stable. Omaya may add optional fields to what it accepts and new vendor formats; it will not remove or rename any field, topic segment or port listed on this page without a published migration period. Fields your gateway sends that Omaya does not list are ignored today and may be adopted later — which is why you may include them freely.