Device and connectivity¶
The sensors and radios: battery, clock, motion, LED, WiFi, Bluetooth, the USB connector and what is plugged into it, VPN and sleep.
Battery¶
| Route | Method | What it does |
|---|---|---|
/api/battery |
GET | The averaged battery voltage in volts, null before the first sample. |
Sleep thresholds are settings, not routes: see Sleep.
Clock¶
All times are UTC; the device never applies a time zone.
| Route | Method | What it does |
|---|---|---|
/api/rtc |
GET | time is the system clock, valid whether it is trusted this boot, rtc the clock chip's own reading (null when it is implausible, as on a fresh board), sntp the network time settings and the last successful sync. |
/api/rtc |
POST | {"epoch":1759900000} sets the system clock and the clock chip from a UTC epoch (2020 to 2099). The way to set the time from a browser on the access point, with no internet. Answers the GET payload. |
/api/rtc/sync |
POST | A network time sync now. Blocks up to about 30 s; 400 when offline, 500 when no server answered, else the GET payload. |
{
"time": "2026-10-09T04:06:57Z",
"valid": true,
"rtc": "2026-10-09T04:06:55Z",
"sntp": {
"enabled": true,
"server": "pool.ntp.org",
"last_sync": "2026-10-09T02:52:49Z"
}
}
Network time syncs on every internet connection and every sync_interval_h after that; ntp_server and ntp_server2 are in the rtc_manager settings.
Motion sensor¶
| Route | Method | What it does |
|---|---|---|
/api/imu |
GET | activity (stationary, active, unknown), acceleration in g on three axes (about 1 g on one axis at rest), die temperature. null fields when the chip cannot be read. |
The motion bit in /api/status is the same information as a flag.
LED¶
The LED has a priority ladder: idle (the configured idle colour), status (activity), alert (yours), critical (the firmware's do-not-power-off indication during an update). A client can set and release the alert slot only; the firmware's own indications stay honest.
| Route | Method | What it does |
|---|---|---|
/api/led |
GET | What the LED shows now: the winning priority, the mode (off, solid, blink_slow, blink_fast, breathe) and the colour. |
/api/led |
PUT | {"r":0,"g":120,"b":255,"mode":"breathe"} sets the alert indication. Colours 0 to 255, mode optional (solid by default). 400 on bad values. |
/api/led |
DELETE | Releases the alert indication; the LED falls back to status or idle. |
The idle colour and mode are the led_manager settings. breathe is timed by the LED chip itself: about one second up, one down, two dark.
WiFi¶
| Route | Method | What it does |
|---|---|---|
/api/wifi/status |
GET | Station and access point state: joined, the address, the access point's address and client count, whether the access point still has the factory password, DNS servers, and sta_attempt (the last join attempt: network, reason code of the last disconnect, consecutive failures, whether that network is now tried after the others). |
/api/wifi/scan |
GET | Scans for networks. Blocks about 2 s; a phone on the access point may lose it for the scan. |
/api/wifi/try |
POST | {"ssid":"...","password":"...","channel":6} tries a network now without saving anything: joins, waits for an address (20 s in all), reports, lets go, and rejoins the configured network. 202 and the trial's status. channel (optional, from the scan) limits the search to that channel. 409 while a trial runs or when the station is off. |
/api/wifi/try |
GET | The trial's state and result: connected, password (wrong password), not_found, refused, no_ip or timeout, with the address, signal and time it took when it worked. The last result stays until the next trial. |
{
"enabled": true,
"sta_connected": true,
"ip": "10.42.1.194",
"ap_started": false,
"ap_default_password": false,
"clients": 0,
"ap_ip": "192.168.4.1",
"dns": [
"10.42.1.1",
"1.1.1.1"
],
"sta_attempt": {
"ssid": "WICAN_TEST_AP",
"reason": 8,
"fail_count": 0,
"deprioritised": false
}
}
{
"networks": [
{
"ssid": "ESPNetLink_457FED",
"rssi": -20,
"channel": 6,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:01"
},
{
"ssid": "Network-1",
"rssi": -27,
"channel": 1,
"auth_mode": "WPA2_WPA3_PSK",
"bssid": "02:00:00:00:00:02"
},
{
"ssid": "WiCAN_32E3C5",
"rssi": -34,
"channel": 6,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:03"
},
{
"ssid": "WICAN_TEST_AP",
"rssi": -45,
"channel": 6,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:04"
},
{
"ssid": "Network-2",
"rssi": -55,
"channel": 10,
"auth_mode": "WPA2_WPA3_PSK",
"bssid": "02:00:00:00:00:05"
},
{
"ssid": "Network-3",
"rssi": -56,
"channel": 2,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:06"
},
{
"ssid": "Network-4",
"rssi": -57,
"channel": 10,
"auth_mode": "WPA2_WPA3_PSK",
"bssid": "02:00:00:00:00:07"
},
{
"ssid": "Network-5",
"rssi": -57,
"channel": 10,
"auth_mode": "WPA2_WPA3_PSK",
"bssid": "02:00:00:00:00:08"
},
{
"ssid": "",
"rssi": -57,
"channel": 10,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:09"
},
{
"ssid": "Network-6",
"rssi": -65,
"channel": 11,
"auth_mode": "WPA2_WPA3_PSK",
"bssid": "02:00:00:00:00:0a"
},
{
"ssid": "Network-7",
"rssi": -80,
"channel": 6,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:0b"
},
{
"ssid": "Network-8",
"rssi": -81,
"channel": 5,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:0c"
},
{
"ssid": "Network-9",
"rssi": -81,
"channel": 5,
"auth_mode": "WPA_WPA2_PSK",
"bssid": "02:00:00:00:00:0d"
},
{
"ssid": "Network-10",
"rssi": -82,
"channel": 6,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:0e"
},
{
"ssid": "",
"rssi": -82,
"channel": 2,
"auth_mode": "WPA_WPA2_PSK",
"bssid": "02:00:00:00:00:0f"
},
{
"ssid": "Network-11",
"rssi": -86,
"channel": 2,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:10"
},
{
"ssid": "Network-7",
"rssi": -88,
"channel": 11,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:11"
},
{
"ssid": "",
"rssi": -89,
"channel": 2,
"auth_mode": "WPA_WPA2_PSK",
"bssid": "02:00:00:00:00:12"
},
{
"ssid": "Network-12",
"rssi": -89,
"channel": 11,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:13"
},
{
"ssid": "Network-13",
"rssi": -89,
"channel": 11,
"auth_mode": "WPA2_WPA3_PSK",
"bssid": "02:00:00:00:00:14"
},
{
"ssid": "",
"rssi": -89,
"channel": 12,
"auth_mode": "WPA_WPA2_PSK",
"bssid": "02:00:00:00:00:15"
},
{
"ssid": "Network-14",
"rssi": -90,
"channel": 11,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:16"
},
{
"ssid": "Network-15",
"rssi": -91,
"channel": 1,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:17"
},
{
"ssid": "",
"rssi": -91,
"channel": 7,
"auth_mode": "WPA2_PSK",
"bssid": "02:00:00:00:00:18"
}
]
}
WiFi configuration (mode, access point, your networks, trust) is the wifi_manager settings document; there are no other WiFi routes.
Bluetooth LE¶
| Route | Method | What it does |
|---|---|---|
/api/ble |
GET | The link (connected, secured = paired and authenticated, the negotiated payload size, the PHYs in use) and the stream channels with their byte counters and loss indicators. |
/api/ble/blast |
POST | {"bytes":N,"chunk":490} pumps a counting pattern over the data pipe, for throughput tests. 409 without a link. |
{
"enabled": false,
"connected": false,
"secured": false,
"pairing_enabled": true,
"max_payload": 20,
"phy": "1m",
"advertising": "legacy",
"phy_tx": 0,
"phy_rx": 0,
"blast": {
"running": false,
"bytes": 0,
"target": 0,
"ms": 0
},
"channels_cap": 4,
"channels": [
{
"name": "http",
"uuid_out": "FFF3",
"uuid_in": "FFF4",
"rx_size": 32768,
"rx_bytes": 0,
"tx_bytes": 0,
"rx_overflow": 0,
"tx_timeouts": 0,
"tx_link_down": 0,
"rx_pending": 0,
"out": "none",
"out_modes": "both",
"tx_notifications": 0,
"tx_indications": 0
}
]
}
BLE is off by default; ble_manager.enabled turns it on (reboot to apply). The GATT contract is on the BLE API pages.
USB connector¶
| Route | Method | What it does |
|---|---|---|
/api/usb |
GET | The connector's role and what is on it: device_present, host_active, the enumerated device (vendor and product ids, class, a product name for the devices WiCAN knows), the USB Ethernet uplink (eth_connected, driver, address), the vbus state. |
/api/usb/acm |
GET | Whether a serial (CDC-ACM) device is bound, whether anyone is reading it, and what it turned out to be: console (an ESPNetLink's command prompt) or nmea (a GPS receiver streaming NMEA sentences). |
/api/usb/acm/cmd |
POST | {"cmd":"lte -s","timeout_ms":8000} sends one line to an ESPNetLink's own console over the cable and returns its reply. 409 when no device is bound or it is busy. |
/api/gps |
GET | The last live fix: position, accuracy, altitude, speed in m/s, heading, satellites, age. {"valid":false} without a fix. Comes from an ESPNetLink or a plain USB GPS receiver. |
{
"enabled": true,
"device_present": true,
"host_active": true,
"eth_connected": false,
"driver": "",
"ip": "",
"attaches": 1,
"vid": "0000",
"pid": "0000",
"vbus": true,
"device": null
}
The same fix is published as AutoPID parameters (gps_latitude, gps_longitude, gps_altitude, gps_speed in km/h, gps_heading, gps_satellites), so it reaches Home Assistant, the dashboard, the logger and the rules like any vehicle value. Only a live fix is reported; a cached position is never presented as current.
ESPNetLink¶
| Route | Method | What it does |
|---|---|---|
/api/espnetlink |
GET | The dongle as internet uplink: mode (wifi_modem, usb_ncm, usb_rndis), pairing state, the dongle's firmware and API level, its health (LTE connected, signal, operator, SIM state, connection stage, carrier address, GPS fix) and the carrier settings relayed to it. |
/api/espnetlink/pair |
POST | {"ssid":"...","password":"..."} pairs by hand: stores the dongle's access point as a trusted fallback network. Reboot to apply. 400 when no fallback slot is free. |
/api/espnetlink/repair |
POST | Cycles the dongle's USB power so it enumerates again and pairing restarts. 409 when the USB host is not up. |
{
"enabled": true,
"mode": "wifi_modem",
"auto_pair": true,
"paired": true,
"ssid": "ESPNetLink_457FED",
"device_id": "14c19f457fed",
"uplink": "wifi",
"on_link": false,
"host": "",
"pair_blocked_factory_pw": false,
"last_error": "",
"dongle_fw": "",
"dongle_api": 0,
"dongle_api_min": 7,
"health_unsupported": false,
"apn": "",
"carrier_synced": true,
"carrier_note": "",
"usb": {
"attached": false,
"pair_state": "idle",
"cuts": 0,
"vbus_cycles": 0,
"errors": 0
},
"gps": {
"valid": false
},
"dongle": {
"valid": true,
"lte_connected": true,
"rssi_dbm": -55,
"operator": "ALDI Mobile",
"network_type": "eMTC",
"gps_fix": true,
"usb_data": false,
"sim": "ready",
"stage": "connected",
"ip": "10.195.16.154",
"age_s": 4150
},
"polls": 133,
"failures": 1,
"link_ups": 1
}
Zero-touch pairing is held (pair_blocked_factory_pw) while the access point still has the factory password: set your own, and the next restart completes the pairing.
VPN¶
| Route | Method | What it does |
|---|---|---|
/api/vpn |
GET | State (disabled, waiting, connecting, connected), type (wireguard or tailscale), endpoint, counters. |
/api/vpn/keygen |
POST | Generates a WireGuard key pair on the device; the private key goes straight into the pending settings and never leaves. Answers the public key. |
{
"state": "disabled",
"type": "wireguard",
"endpoint": "203.0.113.10:51820",
"ts_ip": "",
"ts_peers": 0,
"connects": 0,
"failures": 0,
"uptime_s": 0
}
Sleep¶
| Route | Method | What it does |
|---|---|---|
/api/sleep |
GET | Sleep state and the countdown: state (normal, low_voltage, sleeping, wake_pending), the voltage, the sleep and wake thresholds, pending (which rule will put the device to sleep first: delay or critical) and sleep_in_s, the holds used this boot. |
/api/sleep/hold |
POST | {"minutes":10} keeps the device awake that much longer (1 to 30, three holds per boot). 409 when nothing is counting down or the holds are used up. |
{
"enabled": true,
"state": "normal",
"voltage": 13.5,
"sleep_v": 13.1,
"wake_v": 13.2,
"naps": 0,
"pending": "none",
"sleep_in_s": 0,
"critical_v": 11.9,
"critical_s": 300,
"hold_s": 0,
"holds_left": 3,
"holds_max": 3
}
Two rules put an awake device to sleep. The sleep delay: when the battery stays under sleep_v for sleep_delay_min. The critical floor: when it stays under 11.90 V for 5 minutes, whether sleep is enabled or not. pending names the one that will fire first. The web interface shows the countdown in its header and offers the hold a minute before the end. Thresholds and delays are the sleep_manager settings; there is no route to force a sleep.