Zap Local API

Complete endpoint documentation for the Zap controller firmware local REST API.

Complete endpoint documentation for the controller firmware.

Table of Contents

  1. System & Network
  2. Device Management
  3. Diagnostics
  4. Identity & Security
  5. Utilities
  6. Reference

Base URL

All endpoints prefixed with /api/.

Example: http://192.168.1.100/api/system


System & Network

GET /api/system

System info, memory, WiFi status, uptime.

Response (200):

{
  "time_utc_sec": 1697515200,
  "uptime_seconds": 123456,
  "temperature_celsius": 0.0,
  "memory_kb": {
    "total": 4096.0,
    "free": 2560.0,
    "used": 1536.0,
    "percent_used": 37.5,
    "min": 2048.0,
    "largest": 1024.0
  },
  "processes_average": {
    "last_1min": 0,
    "last_5min": 0,
    "last_15min": 0
  },
  "logging": {
    "enabled": true,
    "level": 4
  },
  "zap": {
    "deviceId": "abc123",
    "cpuFreqMHz": 160,
    "flashSizeMB": 0.0,
    "sdkVersion": "v5.1",
    "firmwareVersion": "1.0.0",
    "publicKey": "04a1b2c3...",
    "network": {
      "wifiStatus": "connected",
      "wifiConnected": true,
      "localIP": "192.168.1.100",
      "ssid": "MyNetwork",
      "rssi": -45,
      "internetConnected": true,
      "mqttConnected": true
    }
  }
}

POST /api/system/reboot

Reboot device after 5s delay.

Response (200):

{
  "status": "success",
  "message": "System is rebooting in 5 seconds"
}

POST /api/system/factory-reset

Factory reset the device. Clears all device configurations, MQTT configuration, and WiFi credentials, then reboots.

Response (200):

{
  "message": "Factory reset successful. Rebooting in 2 seconds."
}

Errors:

  • 500 - Reset operation failed

POST /api/system/log_stream

Toggle real-time log streaming to MQTT. When enabled, device logs are published to gateways/{gateway_id}/logs/raw.

Request:

{
  "enabled": true,
  "level": 4
}

level is optional (default: 4/INFO). Levels: 1=ERROR, 2=WARN, 3=OK, 4=INFO, 5=DEBUG, 6=VERBOSE.

Response (200):

{
  "enabled": true,
  "status": "ok"
}

GET /api/network

Network connectivity status. Returns the same data as the network object in GET /api/system.

Response (200):

{
  "wifiStatus": "connected",
  "wifiConnected": true,
  "localIP": "192.168.1.100",
  "ssid": "MyNetwork",
  "rssi": -45,
  "internetConnected": true,
  "mqttConnected": true
}

When disconnected, only wifiStatus, wifiConnected, and internetConnected are returned.

GET /api/wifi

Get WiFi status and scan results.

Response (200):

{
  "ssids": ["Network1", "Network2"],
  "connected": "Network1"
}

connected is null if not connected.

POST /api/wifi

Set WiFi credentials and connect.

Request:

{
  "ssid": "MyNetwork",
  "psk": "mypassword"
}

Response (200):

{
  "status": "success",
  "message": "WiFi credentials updated and connected"
}

Errors:

  • 400 - Missing ssid or psk
  • 500 - WiFi connect failed

DELETE /api/wifi

Clear WiFi credentials and disconnect.

Response (200):

{
  "status": "success",
  "message": "WiFi credentials cleared, disconnecting in 5 seconds"
}

GET /api/wifi/scan

Initiate a WiFi scan. Results will be available in GET /api/wifi.

Response (200):

{
  "status": "sucess",
  "message": "scan initiated"
}

POST /api/ble/stop

Stop the BLE service.

Response (200):

{
  "status": "success",
  "message": "BLE stopping..."
}

GET /api/system/mqtt

Get current MQTT broker configuration.

Response (200):

{
  "mqtt_connection_str": "mqtt://broker.example.com:1883",
  "mqtt_connection_fallback_str": ""
}

Both fields are empty strings when no MQTT config is set.

POST /api/system/mqtt

Set MQTT broker configuration. Triggers a reboot.

Request:

{
  "mqtt_connection_str": "mqtt://broker.example.com:1883",
  "mqtt_connection_fallback_str": "mqtt://fallback.example.com:1883"
}

At least one of mqtt_connection_str or mqtt_connection_fallback_str is required. Both are optional individually.

Response (200):

{
  "status": "success",
  "message": "MQTT config updated, reboot scheduled"
}

Errors:

  • 400 - Empty body or missing both connection strings

DELETE /api/system/mqtt

Remove MQTT broker configuration. Triggers a reboot.

Response (200):

{
  "status": "success",
  "message": "MQTT config removed from NVS, reboot scheduled"
}

Device Management

GET /api/devices/supported

List supported device profiles.

Response (200):

{
  "count": 12,
  "batteries": [],
  "ev_chargers": [],
  "energy_meters": [
    {
      "display_name": "P1 Meter",
      "profile": "p1_meter",
      "device_type": "energy_meter",
      "connection_types": ["p1_uart"]
    },
    {
      "display_name": "Fronius Smart Meter",
      "profile": "fronius_smart_meter",
      "device_type": "energy_meter",
      "connection_types": ["modbus_tcp"]
    }
  ],
  "inverters": [
    {
      "display_name": "Sungrow",
      "profile": "sungrow",
      "device_type": "inverter",
      "connection_types": ["modbus_tcp", "modbus_rtu"]
    },
    {
      "display_name": "Solis",
      "profile": "solis",
      "device_type": "inverter",
      "connection_types": ["modbus_tcp", "modbus_rtu"]
    }
  ],
  "v2x_chargers": [
    {
      "display_name": "Ambibox",
      "profile": "ambibox",
      "device_type": "v2x_charger",
      "connection_types": ["mqtt"]
    }
  ]
}

GET /api/devices/types

List all possible connection types and their required/optional parameters for creating a device.

Response (200):

{
  "connection_types": [
    {
      "type": "modbus_tcp",
      "description": "Modbus TCP connection",
      "parameters": [
        { "name": "ip", "type": "string", "required": true },
        { "name": "port", "type": "integer", "required": true, "default": 502 },
        { "name": "unit_id", "type": "integer", "required": true, "default": 1 },
        { "name": "profile", "type": "string", "required": true }
      ]
    },
    {
      "type": "modbus_rtu",
      "description": "Modbus RTU connection over RS485",
      "parameters": [
        { "name": "baud_rate", "type": "integer", "required": true, "default": 9600 },
        { "name": "unit_id", "type": "integer", "required": true, "default": 1 },
        { "name": "profile", "type": "string", "required": true },
        { "name": "parity", "type": "integer", "required": false, "default": 0, "description": "0=None, 1=Odd, 2=Even" },
        { "name": "data_bits", "type": "integer", "required": false, "default": 8 },
        { "name": "stop_bits", "type": "integer", "required": false, "default": 1 }
      ]
    },
    {
      "type": "p1_uart",
      "description": "P1 Port (DSMR) connection",
      "parameters": [
        { "name": "manufacturer", "type": "string", "required": false },
        { "name": "baud_rate", "type": "integer", "required": true, "default": 115200 },
        { "name": "data_bits", "type": "integer", "required": true, "default": 8 },
        { "name": "parity", "type": "string", "required": false, "default": "none", "description": "none, even, odd" },
        { "name": "stop_bits", "type": "integer", "required": false, "default": 1 }
      ]
    },
    {
      "type": "mqtt",
      "description": "MQTT connection",
      "parameters": [
        { "name": "broker_host", "type": "string", "required": true },
        { "name": "broker_port", "type": "integer", "required": true, "default": 1883 },
        { "name": "username", "type": "string", "required": false },
        { "name": "password", "type": "string", "required": false },
        { "name": "client_id", "type": "string", "required": false }
      ]
    }
  ]
}

GET /api/devices

List all configured devices. Each entry is the persisted config augmented with runtime:

  • connected: boolean - current connection state
  • last_harvest: unix ms timestamp of most recent DER data (omitted if none yet)
  • path: string - the path to the configuration file in SPIFFS (e.g. spiffs/devices/6178e775.json)
  • ders: per-DER entries (pv/battery/meter/v2x_charger) with publish flags; PV entries also expose installed_power (W) alongside persisted rated_power (W), and battery capacity (Wh)

Response (200):

{
  "count": 2,
  "devices": [
    {
      "type": "modbus_tcp",
      "profile": "sungrow",
      "device_type": "inverter",
      "sn": "INV003SIM03",
      "ip": "192.168.1.60",
      "port": 502,
      "unit_id": 1,
      "connected": true,
      "last_harvest": 1761832393075,
      "path": "spiffs/devices/6178e775.json",
      "ders": [
        {
          "type": "pv",
          "enabled": false,
          "installed_power": 9500,
          "rated_power": 10000
        },
        {
          "type": "battery",
          "enabled": false,
          "rated_power": 5000,
          "capacity": 10000
        },
        {
          "type": "meter",
          "enabled": false
        }
      ]
    },
    {
      "type": "mqtt",
      "profile": "ambibox",
      "device_type": "v2x_charger",
      "sn": "ambibox_192.168.1.50",
      "broker_host": "192.168.1.50",
      "broker_port": 1883,
      "username": "user",
      "client_id": "esp32_local",
      "connected": false,
      "ders": [
        {
          "type": "v2x_charger",
          "enabled": false,
          "capacity": 0
        }
      ]
    }
  ]
}

POST /api/devices

Add device. Connects immediately, saved only on success.

Modbus TCP:

{
  "type": "modbus_tcp",
  "ip": "192.168.1.60",
  "port": 502,
  "unit_id": 1,
  "profile": "sungrow"
}

Modbus RTU (RS-485):

{
  "type": "modbus_rtu",
  "baud_rate": 9600,
  "unit_id": 1,
  "profile": "sungrow",
  "parity": 0
}

On ESP32-C3 (ZAP), Modbus RTU uses UART0 (TX=GPIO7, RX=GPIO5, RTS=GPIO6). P1 and RTU can run simultaneously.

P1 UART:

{
  "type": "p1_uart",
  "manufacturer": "MyMeter",
  "baud_rate": 115200,
  "data_bits": 8,
  "parity": "none",
  "stop_bits": 1
}

P1 UART Configuration Options:

  • baud_rate: Integer > 0 (e.g., 9600, 115200)
  • data_bits: 5, 6, 7, 8
  • parity: "none", "even", "odd"
  • stop_bits: 1, 2, "1.5"

MQTT (Ferroamp EnergyHub):

{
  "type": "mqtt",
  "profile": "ferroamp",
  "broker_host": "192.168.1.70",
  "broker_port": 1883,
  "username": "extapi",
  "password": "your_password"
}

MQTT (Ambibox EV Charger):

{
  "type": "mqtt",
  "profile": "ambibox",
  "broker_host": "192.168.1.70",
  "broker_port": 1884,
  "username": "external-ems",
  "password": "your_password"
}

Response (201):

{
  "message": "Device added and connected",
  "sn": "INV003SIM03",
  "ders": [
    {
      "type": "pv",
      "enabled": false,
      "rated_power": 10000
    },
    {
      "type": "battery",
      "enabled": false,
      "rated_power": 0,
      "capacity": 0
    },
    {
      "type": "meter",
      "enabled": false
    }
  ]
}

Errors:

  • 400 - Invalid config
  • 409 - Device exists or conflicts (UART/TCP endpoint)
  • 503 - Connection failed or insufficient memory (requires >55KB free heap)

GET /api/devices/{sn}

Get device details by serial number. Not yet implemented — returns 501.

Response (501):

{ "message": "Not implemented" }

Errors:

  • 400 - Missing serial number
  • 404 - Device not found
  • 501 - Not implemented

DELETE /api/devices/{sn}

Remove device by serial number.

Example: DELETE /api/devices/INV003SIM03

Response (200):

{ "message": "Device removed" }

Errors:

  • 400 - Missing serial number
  • 404 - Device not found

DELETE /api/devices

Remove all devices. Clears runtime memory and deletes all device configurations from storage.

Example: DELETE /api/devices

Response (200):

{ "message": "All devices removed" }

Errors:

  • 500 - Failed to clear devices (e.g. SPIFFS error)

GET /api/devices/{sn}/data/json

Latest device data snapshot in JSON format.

Example: GET /api/devices/INV003SIM03/data/json

Response (200):

{
  "pv": {
    "type": "pv",
    "timestamp": 1761832393075,
    "read_time_ms": 472,
    "make": "sungrow",
    "W": -2500,
    "total_generation_Wh": 22698000
  },
  "battery": {
    "type": "battery",
    "timestamp": 1761832393075,
    "read_time_ms": 156,
    "make": "sungrow",
    "W": -500,
    "V": 48.2,
    "A": -10.4,
    "SoC_nom_fract": 0.85
  },
  "meter": {},
  "v2x_charger": {
    "type": "v2x_charger",
    "timestamp": 1761832393075,
    "read_time_ms": 0,
    "make": "ambibox",
    "status": "charging",
    "W": 7400,
    "A": 32.0,
    "V": 230.0,
    "Hz": 50.0,
    "L1_V": 230.0,
    "L1_A": 10.7,
    "L1_W": 2461.0,
    "L2_V": 229.5,
    "L2_A": 10.6,
    "L2_W": 2432.7,
    "L3_V": 230.2,
    "L3_A": 10.8,
    "L3_W": 2486.2,
    "dc_W": -7200,
    "dc_V": 400.0,
    "dc_A": -18.0,
    "vehicle_soc_fract": 0.45,
    "ev_min_energy_req_Wh": 5000,
    "ev_max_energy_req_Wh": 40000,
    "session_charge_Wh": 12500,
    "session_discharge_Wh": 0,
    "total_charge_Wh": 125000,
    "total_discharge_Wh": 45000,
    "lower_limit_W": [-11000, 0, 1400],
    "upper_limit_W": [-1400, 0, 11000],
    "capacity_Wh": 77000,
    "rated_power_W": 11000
  },
  "version": "v1",
  "format": "json"
}

Errors:

  • 204 - No harvest data yet
  • 404 - Device not found
  • 503 - Device busy (lock timeout)

GET /api/devices/{sn}/data/raw

Raw device data snapshot. For P1 meters, returns the raw OBIS strings as received from the meter. For other device types, returns device-specific raw data if available.

Example: GET /api/devices/p1_meter/data/raw

Response (200) - P1 Meter:

{
  "format": "p1_uart",
  "data": {
    "ts": 1770302863728,
    "device_id": "p1_meter",
    "obis": [
      "1-3:0.2.8(50)",
      "0-0:1.0.0(251021091842S)",
      "1-0:1.8.1(004121.646*kWh)",
      "1-0:1.8.2(004416.112*kWh)",
      "1-0:2.8.1(002029.530*kWh)",
      "1-0:2.8.2(004760.021*kWh)",
      "1-0:1.7.0(00.000*kW)",
      "1-0:2.7.0(00.445*kW)",
      "1-0:32.7.0(238.1*V)",
      "1-0:52.7.0(236.6*V)",
      "1-0:72.7.0(237.9*V)"
    ]
  }
}

Fields:

  • format: Device connection type (e.g., p1_uart, modbus_tcp)
  • data: Raw payload object (format varies by device type)
    • P1 meters: ts (timestamp ms), device_id, obis (array of OBIS strings)

Errors:

  • 204 - No raw data available yet
  • 404 - Device not found
  • 503 - Device busy

GET /api/devices/{sn}/data/debug

Debug information for troubleshooting device communication issues. For P1 meters, returns the hex dump and ASCII representation of the last frame that failed to decode.

Example: GET /api/devices/p1_meter/data/debug

Response (200) - P1 Meter with failed frame:

{
  "device_type": "p1_uart",
  "sn": "p1_meter",
  "last_failed_ts": 1770302863728,
  "last_failed_type": 0,
  "last_failed_size": 56,
  "last_failed_hex": "2F 58 4D 58 35 4C 47 42 42 46 46 46 46 31 30 30 31 32 33 34 35 36 37 38 ...",
  "last_failed_ascii": "/XMX5LGBBFFFF10012345678..."
}

Response (200) - P1 Meter with no failed frames:

{
  "device_type": "p1_uart",
  "sn": "p1_meter",
  "last_failed_hex": "",
  "message": "No failed frames recorded yet"
}

Response (200) - Non-P1 device:

{
  "sn": "INV003SIM03",
  "message": "Debug info not available for this device type"
}

Fields:

  • device_type: Device connection type
  • last_failed_ts: Timestamp (ms) when the frame was received
  • last_failed_type: Frame type (0=ASCII, 1=HDLC, 2=MBUS)
  • last_failed_size: Size of the failed frame in bytes
  • last_failed_hex: Hex dump of frame bytes (up to 256 bytes)
  • last_failed_ascii: ASCII representation (non-printable chars replaced with '.')

Failed frames are also logged to the MQTT log stream with the first 64 bytes in hex format.

Errors:

  • 404 - Device not found

POST /api/devices/{sn}/types

Enable/disable publishing for detected DERs on a device. Only included DERs in the request are modified. Omitted entries remain unchanged.

By default, all detected DERs have enabled = false on first connection.

Request:

{
  "ders": [
    { "type": "pv", "enabled": true },
    { "type": "battery", "enabled": false }
  ]
}

Response (200):

{ "message": "Types updated", "sn": "INV003SIM03" }

Errors:

  • 400 - Missing ders array
  • 404 - Device not found
  • 500 - Failed to persist config

GET /api/devices/{sn}/ders

Fetch publish state plus configured DER metadata. The fields returned are the user-configurable fields that can be updated via POST.

Response (200):

{
  "sn": "INV003SIM03",
  "ders": [
    {
      "type": "pv",
      "enabled": true,
      "rated_power": 10000,
      "installed_power": 8500
    },
    {
      "type": "battery",
      "enabled": false,
      "rated_power": 4800,
      "capacity": 9600
    },
    {
      "type": "meter",
      "enabled": false
    },
    {
      "type": "v2x_charger",
      "enabled": true,
      "capacity": 50000
    }
  ]
}

DER Field Reference:

DER TypeFieldUnitDescription
pvenabled-Enable/disable publishing
pvrated_powerWInverter rated power
pvinstalled_powerWActual installed panel power (kWp)
batteryenabled-Enable/disable publishing
batteryrated_powerWBattery inverter rated power
batterycapacityWhBattery capacity
meterenabled-Enable/disable publishing
v2x_chargerenabled-Enable/disable publishing

Errors:

  • 404 - Device not found

POST /api/devices/{sn}/ders

Update publish flags and static DER metadata in one request. For PV entries, supply rated_power in watts. For battery entries, supply rated_power (W) and capacity (Wh). For v2x_charger entries, power limits are read-only from the device. enabled is optional here; omit it to leave the current publish state untouched. Only the DERs included in the payload are modified.

Request:

{
  "ders": [
    {
      "type": "pv",
      "rated_power": 10200
    },
    {
      "type": "battery",
      "enabled": true,
      "rated_power": 5000,
      "capacity": 12800
    }
  ]
}

Response (200):

{ "message": "DERs updated", "sn": "INV003SIM03" }

Errors:

  • 400 - Missing ders array
  • 404 - Device not found
  • 500 - Failed to persist config

GET /api/devices/{sn}/registers/{address}

Read Modbus register(s).

Example: GET /api/devices/INV003SIM03/registers/13045?type=u32&endianness=little&scale_factor=0.1&function_code=3

Query Parameters:

  • type (optional, default: u16) - Data type: u16, i16, u32, i32, f32, u64, i64, str
  • size (optional) - Number of registers (1-125). Required for str, overrides count for others.
  • endianness (optional, default: little) - Byte order: little or big
  • scale_factor (optional, default: 1.0) - Multiply raw value (not for str)
  • function_code (optional, default: 4) - 3 (holding) or 4 (input)

Response (200):

{
  "address": 13045,
  "value": 523.4,
  "type": "u32",
  "registers": 2
}

Bulk read (no type, only size):

{
  "address": 13045,
  "count": 10,
  "values": ["0x0001", "0x0002", "0x0003"]
}

String read:

{
  "address": 4989,
  "value": "SH10RT12345",
  "type": "str",
  "registers": 10
}

Errors:

  • 400 - Invalid type/size or non-Modbus device
  • 404 - Device not found
  • 500 - Read failed
  • 503 - Device busy

POST /api/devices/{sn}/registers

Write one or more holding registers (FC 0x06). Each key in the registers object is an address, each value is a 16-bit register value. Writes are executed sequentially with a 50 ms inter-write delay.

Example: POST /api/devices/INV003SIM03/registers

Request:

{
  "registers": {
    "33010": 5000,
    "33011": 1234
  }
}

Response (200):

{
  "written": 2,
  "failed": 0
}

Errors:

  • 400 - Missing registers object or non-Modbus device
  • 404 - Device not found
  • 500 - One or more writes failed (check written/failed counts)
  • 503 - Device busy (lock timeout)

Diagnostics

POST /api/network/port-check

Check if a TCP port is open on a remote host. Useful for verifying network connectivity to Modbus devices or brokers before adding them.

Request:

{
  "host": "192.168.1.60",
  "port": 502,
  "timeout": 2000
}

timeout is optional (default: 2000ms).

Response (200):

{
  "host": "192.168.1.60",
  "port": 502,
  "open": true
}

Errors:

  • 400 - Missing host or port

POST /api/modbus/read

Ad-hoc Modbus register read without requiring a registered device. Connects, reads, and disconnects automatically.

Request:

{
  "ip": "192.168.1.60",
  "port": 502,
  "unit_id": 1,
  "register": 13001,
  "count": 5,
  "register_type": "holding"
}
FieldRequiredDefaultDescription
ipYes-Modbus device IP
portYes-Modbus device port
unit_idYes-Modbus unit/slave ID
registerYes-Starting register address
countNo1Number of registers to read (1-125)
register_typeNo"holding""holding" (function code 3) or "input" (function code 4)

Response (200):

{
  "ip": "192.168.1.60",
  "port": 502,
  "unit_id": 1,
  "register": 13001,
  "register_type": "holding",
  "values": [1, 0, 5000, 0, 100]
}

Errors:

  • 400 - Missing required field or invalid config
  • 502 - Connection failed or read failed

Identity & Security

GET /api/crypto

Get device identity information.

Response (200):

{
  "deviceName": "software_zap",
  "serialNumber": "abc123def456",
  "publicKey": "04a1b2c3..."
}

POST /api/crypto

Sign the device ID concatenated with a wallet address. Used to prove device identity during initialization.

Request:

{
  "wallet": "0xYourWalletAddress"
}

Response (200):

{
  "idAndWallet": "abc123def456:0xYourWalletAddress",
  "signature": "3045022100..."
}

Errors:

  • 400 - Missing wallet field

POST /api/crypto/sign

Sign a message with the device's private key.

Request:

{
  "message": "optional-message-content",
  "timestamp": "optional-timestamp"
}

Response (200):

{
  "sign": "3045022100...",
  "message": "optional-message-content|nonce|timestamp|serial"
}

Errors:

  • 400 - Pipe character (|) in message or timestamp

GET /api/name

Get device name (ID).

Response (200):

{
  "name": "abc123def456"
}

Utilities

GET /api/debug

Get debug report.

Response (200):

{
  "status": "success",
  "heap": {},
  "tasks": []
}

POST /api/echo

Echo the request body.

Request:

{ "any": "content" }

Response (200):

{
  "echo": "{\"any\":\"content\"}"
}

Reference

Status Codes

CodeMeaning
200OK - request succeeded
201Created - resource was created
202Accepted - async operation queued
204No Content - no data available yet
207Multi-Status - partial success (some operations failed)
400Bad Request - invalid JSON, missing fields, or bad parameter values
404Not Found - resource does not exist
405Method Not Allowed - wrong HTTP verb for this endpoint
409Conflict - resource already exists or operation conflicts
500Internal Server Error - operation failed
501Not Implemented - endpoint exists but feature is not yet built
502Bad Gateway - upstream connection or communication failure
503Service Unavailable - device busy, connection failed, or insufficient memory

Examples

Add TCP device:

curl -X POST http://192.168.1.100/api/devices \
  -H "Content-Type: application/json" \
  -d '{
    "type": "modbus_tcp",
    "ip": "192.168.1.50",
    "port": 502,
    "unit_id": 1,
    "profile": "sungrow"
  }'

Add MQTT device (Ferroamp):

curl -X POST http://192.168.1.100/api/devices \
  -H "Content-Type: application/json" \
  -d '{
    "type": "mqtt",
    "profile": "ferroamp",
    "broker_host": "192.168.1.70",
    "broker_port": 1883,
    "username": "extapi",
    "password": "your_password"
  }'

Add P1 UART device:

curl -X POST http://192.168.1.100/api/devices \
  -H "Content-Type: application/json" \
  -d '{
    "type": "p1_uart",
    "manufacturer": "MyMeter",
    "baud_rate": 115200,
    "data_bits": 8,
    "parity": "none",
    "stop_bits": 1
  }'

Read register (scaled):

curl "http://192.168.1.100/api/devices/INV003SIM03/registers/13045?type=u32&scale_factor=0.1"

Write registers:

curl -X POST http://192.168.1.100/api/devices/INV003SIM03/registers \
  -H "Content-Type: application/json" \
  -d '{"registers": {"33010": 5000}}'

Get device data (JSON):

curl http://192.168.1.100/api/devices/INV003SIM03/data/json

Get device data (Raw/OBIS):

curl http://192.168.1.100/api/devices/p1_meter/data/raw

Remove device:

curl -X DELETE http://192.168.1.100/api/devices/INV003SIM03

Remove all devices:

curl -X DELETE http://192.168.1.100/api/devices

Notes

  • Device configs persist in /spiffs/devices/{sn}.json
  • Auto-reconnect every 10s for disconnected devices
  • Modbus timeout: 1000ms per request
  • UART pins fixed by firmware (RTU only configures baud/unit_id)
  • Register read/write defaults to Input registers (use function_code=3 for Holding)
  • Wrong HTTP method returns 405
  • ESP32-C3 (ZAP): P1 (UART1) and Modbus RTU (UART0) can run simultaneously
  • ZAP Hardware Variants: RS-485 requires the RS-485 module variant. The firmware detects the hardware via SENSE pins and warns if attempting RS-485 on incompatible hardware.