Device Status
Device status is the device's battery level and its SNR margin. The server requests the status with the DevStatusReq MAC command, and the device answers with DevStatusAns; both commands have CID 0x06. ONS asks on its own, nothing needs to be enabled. The server adapts the request frequency to how often the device reports and whether it answers. The server passes the received values in every uplink message, shows them in the web interface and stores them in the database.
When the server requests the status
Each request is a downlink frame: it takes gateway airtime and a device receive window. So the server asks rarely, and devices that do not answer even more rarely. It calculates the time until the next request as follows:
due = max(device interval, K × frame period) × 2^misses, growth from misses capped by the ceiling
| Term | What it is | Default |
|---|---|---|
| Device interval | The dev_status_req_interval setting, in seconds. The lower bound: requests never go out more often. 0 or empty means never ask, see How to change the request frequency | 86400 (one day) |
| K | How many uplink frames there are per request | 20 |
| Frame period | How often the device reports: the server smooths the interval between frames, the weight of a new interval is 0.25. The estimate appears from the second frame; until then the frame multiplier does not apply | — |
| Misses | How many requests in a row went unanswered. Each miss doubles the time | 0 |
| Ceiling | The limit on the growth from misses | 30 days |
A miss is counted when the next request is due and the previous one has still not been answered — once per request. Any DevStatusAns, including one that arrives without a request, resets the misses.
What this gives with K = 20 and a one-day interval:
| Device | How often the server asks |
|---|---|
| A frame once a day, answers | Once every 20 days |
| A frame every 5 minutes, answers | Once a day |
| A frame once a day, does not answer | After 20 days, then once every 30 days |
| A frame every 5 minutes, does not answer | After 1 day, 2, 4, 8, 16 days, then once every 30 days |
| Interval 0 or empty | Does not ask |
The server queues a scheduled request only when it receives an uplink — there are no separate timers. On each uplink it updates the period estimate and, if a request is due, queues DevStatusReq in the device's MAC command queue. The request goes out in a downlink frame in the receive window after that uplink, together with application data if any is queued. The device sends the answer in its next scheduled uplink. When a device of each class receives a downlink is described on the Device classes page.
Piggybacking
If a downlink frame is already going out for another reason — for example application data, another MAC command or the ACK of a confirmed uplink — the server adds DevStatusReq to it ahead of time. The condition: at least half of the due time and at least the device interval have passed since the previous request. Piggybacking never creates a frame of its own.
For devices that report often and answer, the due time equals the interval, so a piggybacked request never goes out before it is due. For a device that does not answer, a piggybacked request goes out in the very first downlink frame after a miss is counted. If such a device gets downlink frames often, the intervals in the table above become one step shorter: with a frame every 5 minutes — 1 day, 1 day, 2, 4, 8, 16 days, then once every 30 days.
There is no piggybacking:
- for multicast groups and class B devices;
- if there is no room left in the frame for the command;
- if the previous request has not been answered yet and the next one is not due yet.
When no request is sent
- The device interval is 0 or empty. The server never asks for the status; server flags do not change this. In earlier ONS versions, devices created through the WebSocket API got an empty interval — their status is not requested.
- The device has just appeared in the server cache — for example, after joining the network: on each join the server rebuilds the device record. The countdown starts from this uplink, and no request goes out.
- The previous DevStatusReq is still in the MAC command queue — it did not fit in a frame. The server does not queue a second one.
- A multicast group. Groups send no uplinks, so they get no status requests.
- A class B device. ONS does not transmit frames to such devices: the request is queued and a
duerow appears in the log, but it never goes on air. No answer comes, and the due time grows as for a device that does not answer. See Device classes. - A failure inside the mechanism. In this case the server does not send the request and writes a warning to its log.
What the answer contains
DevStatusAns carries two bytes: the battery level and the margin.
| Battery byte | Meaning | battery_level |
|---|---|---|
| 0 | The device runs on external power | 0 |
| 1–254 | Battery level | Percent: byte / 254 × 100, a fractional number. 1 → 0.39 %, 254 → 100 % |
| 255 | The device cannot measure the battery level | 255 |
The margin is the SNR at which the device received the request, in dB, from −32 to 31. The server reads the whole second byte: values above 31 become negative by subtracting 64. It does not drop the two upper (RFU) bits: if the device sets them, the margin will be wrong.
The server keeps the last answer in the device cache and writes every answer to the database together with the time of writing (UTC). After the device joins the network again, the cached values are empty until a new answer arrives.
Where the values appear
WebSocket API
Every realtime_data message has the battery_level and margin fields — the latest values known to the server. They repeat in every uplink until a new answer arrives. Before the first answer and after the device joins the network again they are null. The message does not carry the measurement time.
An answer reaches realtime_data one uplink late. The server takes the values for the message when it passes the uplink on, while the MAC commands of the same uplink, DevStatusAns included, are parsed about 0.6 seconds later. So an answer from uplink N appears in realtime_data only in uplink N+1. The answer reaches the database — the chart and MCP — immediately when parsed.
If the application has a JS codec, the status fields are not lost: the server adds the codec result to the message and passes it on.
MQTT, HTTP and ThingsBoard
The MQTT and HTTP integrations publish a body of the same form as realtime_data, with the battery_level and margin fields. The ThingsBoard integration sends battery_level, margin and join_request_time as telemetry.
Web interface
The "Node status" block is in the device card in the "Nodes" section, on the Monitoring tab:
- the header shows the latest margin ("N dB", tooltip "Noise margin") and battery level ("N%");
- the "Noise margin" chart (axis "Margin (dB)") and the "Battery" chart (axis "Charge percentage");
- the period is the field next to the "Number of months" button: from 1 to 12 months, 1 by default. The charts are rebuilt when the button is pressed.
| Shown instead of the percentage | When |
|---|---|
| "In charge", a plug icon; no "Battery" chart | The oldest record in the period has battery level 0 |
| "No actual data"; no "Battery" chart | The latest record has battery level 255 |
| "No actual data" | The latest value on the battery chart is outside 1–100, for example 0 |
Records with battery level 255 are not plotted on the "Battery" chart. A margin above 31 is shown on the chart as 31.
MAC command log
The server writes every scheduled and piggybacked request, and every answer, to the device's MAC command log. In the web interface this is the "MAC command log" block on the Monitoring tab, see Radio parameters and the MAC command log. The command with CID 6 is called DevStatus there; the log can be filtered by it.
| Direction | Status | Payload | Meaning |
|---|---|---|---|
| Req | due | Empty | A request on schedule. The row is written when the request is queued in the MAC command queue |
| Req | free-ride | Empty | A piggybacked request in a frame that went out for another reason |
| Ans | answered | The 2 answer bytes in hex | The device answered |
These are not the sent, acked, rejected and unsolicited statuses described on the "Nodes" page; the interface shows them with a gray label. The source (source) of these rows is dev_status.
REST API
The same data is returned by the server REST API that the web interface uses. It requires signing in and permission to view the device.
POST /api/v1/chart/node_status/?dev_eui=<DevEUI>&period=<months>— the "Node status" chart data for the lastperiodmonths (1 by default), in ascending time order. Thedatafield of the response is an array with one object{"name": "NODE_STATUS", "series": [...]}. Aserieselement:name— the record time,value— the margin (values above 31 are capped at 31),battery—battery_level,external—trueif the battery level is 0. For an inactive device the server responds with 404.GET /api/v1/node/<DevEUI>/mac_log/?cid=6— the MAC command log, newest records first, paged. Filters:cidandstatus. Row fields:time,cid,direction,payload,status,ack_bits,fcnt,source.
MCP
get_device_status— thedev_statusblock:battery_level,marginandtime, the time of the latest record in the database.get_mac_log— the MAC command log with thesourcefield;limitis up to 500, 50 by default.send_mac_command— request the status outside the schedule:data=06(DevStatusReq), the command goes out on FPort 0. Requires themcp:writescope, thechange_nodepermission and the--allow_mac_writeflag of the MCP server. Look for the answer inget_mac_log. No Req row appears in the log for such a request, only the Ans rowanswered.
How to check whether a device answers
- Open the device card in the "Nodes" section, the Monitoring tab.
- In the "MAC command log" block, select the DevStatus command.
- Compare the rows: each Req row (
dueorfree-ride) should be followed in time by an Ans row with theansweredstatus. The newest rows are at the top, so the Ans row appears above its Req row.
If there are no Ans rows, the device does not answer: the interval between requests doubles, up to 30 days. An Ans row without a Req row appears when the request was sent manually, for example with send_mac_command. If there are no rows at all, either the server does not request the status — check the device interval — or the first request is not due yet. The MCP tool get_mac_log returns the same log.
How to change the request frequency
The lower bound is set by the device setting dev_status_req_interval, in seconds:
| Value | What the server does |
|---|---|
| Greater than 0 | Asks no more often than once per that many seconds |
| 0 or empty | Never asks |
The default is 86400 (one day). Devices created in the web interface, through the WebSocket API and through MCP get this value; the create_device_req command does not accept this field.
Of the APIs, only the MCP tool update_device changes the setting: the dev_status_req_interval field, an integer from 0 to 2147483647 or null. Requires the mcp:write scope, the change_node permission and the --allow_node_write flag of the MCP server. After the change the server immediately updates the setting in the device cache. The web interface, the REST API and the WebSocket API do not have this field. The server administrator can also change it in the server administration panel; after saving, the server updates the cache immediately as well.
The other parameters are shared by the whole server. The installation administrator sets them with LWServer startup flags:
| Flag | What it sets | Default |
|---|---|---|
--dsr_frames | K — frames per request. 0 removes only the frame multiplier | 20 |
--dsr_backoff_max | The ceiling on the growth from misses, seconds | 2592000 (30 days) |
--dsr_free_ride | The share of the due time for piggybacking. 0 turns piggybacking off | 0.5 |
The previous rule — a request every dev_status_req_interval seconds regardless of answers — is restored by the set --dsr_frames 0 --dsr_backoff_max 0 --dsr_free_ride 0. It applies only to devices with an interval set: 0 and empty still mean "do not ask".
Good to know
- For devices that report rarely, the status is updated about once per 20 frames: for a device with one frame a day, once every 20 days. The value in
realtime_datamay be old, and the message does not carry the measurement time; the time is available inget_device_statusand on the chart. - After the device joins the network again,
battery_levelandmargininrealtime_dataarenulluntil the device answers. The first request after the join goes out no earlier than one device interval later. - The chart takes the "In charge" mark from the oldest record in the period, not from the latest one.
- The database stores the battery level in an integer column, so on the chart and in MCP it may differ from the fractional value in
realtime_data. - The mechanism state — the period estimate, the time of the previous request, the misses — is kept in the server cache and in Redis (the key lives 30 days after the last write); it is not in the database. If the copy is missing both from server memory and from Redis, the countdown starts over as for a new device: the first request goes out no earlier than one device interval later, and misses are counted from zero.
See also
- Subscribe to real-time data — the
battery_levelandmarginfields of therealtime_datamessage. - Nodes — the Monitoring tab and the MAC command log.
- MQTT — the uplink message body with the status fields.
- Device classes — when a device receives the downlink that carries the request.
- Sending data to a device — the downlink queue and frame statuses.