nexalwarenexalwaredocs
SDK & MCP

SDK Reference (Python)

Install, every method's parameters and return type, and a working example.

nexalware is a typed client for the same "Device Control" endpoints an API key can already call over REST. Zero third-party dependencies, built on urllib from the standard library, so dropping it into any existing agent environment can never trigger a version conflict. Writing in TypeScript instead? See the SDK Reference (TypeScript).

Install

pip install nexalware

Quickstart

from nexalware import NexalwareClient

client = NexalwareClient(api_key="nxw_live_sk_your_key_here")

client.turn_on("dev_a1b2c3")

latest = client.get_latest_telemetry("dev_a1b2c3")
print(latest["state"], latest["telemetry"])

Get an API key from the dashboard (API Keys), and make sure it has a DeviceGrant covering the device(s) and command(s) you call, an ungranted key authenticates fine but every call is rejected with a 403.

Field names match the API's own JSON, not snake_case

Return values are plain dicts (typed as TypedDict for editor/type-checker support), with the same field names the REST API itself uses, e.g. latest["subDeviceId"], not latest["sub_device_id"]. Method and argument names are proper Python snake_case, only the data payloads keep the wire format, so a field never needs translating between what the docs, the API, and this SDK each call it.

NexalwareClient(api_key, base_url=...)

ParamTypeRequiredMeaning
api_keystryesA secret key from the dashboard.
base_urlstrnoOverride for a self-hosted or staging deployment. Defaults to https://api.nexalware.com.
timeoutfloatnoAbort a request after this many seconds. Defaults to 30.0.

Errors

Every method raises NexalwareApiError on a non-2xx response, it never returns a "silent" error value.

AttributeTypeMeaning
statusintHTTP status code.
errorstrShort machine-readable code, e.g. "FORBIDDEN", "NOT_FOUND".
messagestr | NoneHuman-readable reason, same text a dashboard user would see.
detailslist | NoneOnly present on a 400 validation failure.
from nexalware import NexalwareApiError

try:
    client.send_command("dev_a1b2c3", "SET_BRIGHTNESS", params={"level": 60})
except NexalwareApiError as err:
    print(err.status, err.error, err.message)

Methods

Every method below is documented the same way: what each parameter actually means and how to fill it in, then a real example, then the return shape. If you've never touched this platform before, read list_devices and send_command first, almost everything else is a variation on those two.

list_devices(project_id=None)

The devices this key can actually act on, only what its own DeviceGrant(s) cover, never the rest of the account. This is the right first call when you don't already know a device_id, don't guess one or ask the user to dig it out of the dashboard, just ask the API.

Parameters

  • project_id (str, optional) — Pass this only if you want to narrow the list to one project (Nexalware's grouping mechanism for devices, e.g. "Warehouse A" vs "Warehouse B"). Get a valid value by first calling list_devices() with no argument and reading the "project"]["projectId"] field off whatever comes back, there's no separate "list projects" call, projects only ever show up as a field on a device. Leave it as None (the default) to list every device the key can reach.

Example

# Every device this key can see
devices = client.list_devices()
for d in devices:
    print(d["deviceId"], d["name"], "online" if d["isOnline"] else "offline")

# Only devices in one project
warehouse_devices = client.list_devices(project_id="proj_a1b2c3")

Returns List[Device]

FieldTypeMeaning
deviceIdstrUse this everywhere else a method asks for device_id.
namestrWhatever the human named it on the dashboard, e.g. "Garage Light".
boardTypestrFree text set at registration, e.g. "esp32".
appTypestrFree text set at registration, e.g. "Industrial Automation".
deviceStatus"PENDING" | "ACTIVE" | "DISABLED" | "REVOKED"PENDING means credentials were never generated, it will never come online. DISABLED means someone turned it off from the dashboard.
enabledboolTrue only when deviceStatus is ACTIVE.
isOnlineboolLive connection state, not the same as enabled, a disabled device is never online but an enabled one can still be offline.
lastSeenstr | NoneISO timestamp of its last MQTT activity, None if it's never connected.
project{"name", "projectId"} | NoneNone if it was never assigned to a project.

get_commands(device_id)

The command catalog this device accepts, name/label/params-schema per command. Call this before send_command so you know what cmd values are actually valid, sending an unrecognized cmd gets rejected.

Parameters

  • device_id (str, required) — A device's public id, always shaped like dev_ followed by a short alphanumeric string, e.g. "dev_a1b2c3". Get one from list_devices(), never make one up.

Example

commands = client.get_commands("dev_a1b2c3")
# [{"name": "ON", "label": "Turn on", "kind": "ACTION", "paramsSchema": None, "requiresApproval": False}, ...]

Returns List[CommandDefinition]

FieldTypeMeaning
namestrCommand name, use this exact string as cmd in send_command.
labelstrHuman-readable label, e.g. show this in a UI instead of the raw name.
kind"ACTION" | "QUERY"Whether it changes device state or just reads it.
paramsSchemadict | NoneJSON Schema describing what params this command expects. None means it takes no params at all, don't pass any.
requiresApprovalboolIf True, calling send_command with this cmd still succeeds (HTTP-wise) but queues for manual approval instead of executing immediately, see send_command's return shape below.

A device with no custom catalog (nothing configured beyond the basics) returns the legacy set: ON, OFF, TOGGLE, STATUS, none of which take params.

send_command(device_id, cmd, params=None, target=None)

The general-purpose way to actually make a device do something.

Parameters

  • device_id (str, required) — The device to command, from list_devices().
  • cmd (str, required) — Must exactly match a name from get_commands(device_id). Case-sensitive, "on" is not the same as "ON".
  • params (dict, optional) — Only include this if the command's catalog entry has a non-None paramsSchema. For a plain ON/OFF-style command, leave this as None (the default) rather than passing {}. Shape depends entirely on that specific command, e.g. SET_BRIGHTNESS might expect {"level": 60}, read the device's own paramsSchema to know what keys it wants.
  • target (str, optional) — Only set this if device_id is a "master" orchestrating sub-devices and you want to aim the command at one specific sub-device instead of the master itself. If you're doing that, prefer send_sub_device_command below instead, it's the exact same call with clearer argument names, this target parameter exists mainly so send_sub_device_command has something to delegate to.

Example

# No params
client.send_command("dev_a1b2c3", "ON")

# With params
client.send_command("dev_a1b2c3", "SET_BRIGHTNESS", params={"level": 60})

# Aimed at a sub-device behind a master (prefer send_sub_device_command for this)
client.send_command("dev_master1", "OPEN", target="sub_x1y2z3")

Returns {"ok": True, "approvalRequired": bool | None, "approvalId": str | None} — if approvalRequired is True, the command has not run yet, it's sitting in a queue waiting for an Owner/Admin to approve it from the dashboard; approvalId identifies that pending request.

turn_on(device_id) / turn_off(device_id)

Shorthand for send_command(device_id, "ON") / send_command(device_id, "OFF"), for the common case of a plain relay-style device.

Parameters

  • device_id (str, required) — The device to turn on/off.

Example

client.turn_on("dev_a1b2c3")
client.turn_off("dev_a1b2c3")

Returns {"ok": True}

get_telemetry(device_id, metric=None, limit=None, since=None)

Historical sensor/telemetry readings a device has reported, newest first.

Parameters

  • device_id (str, required) — The device whose history you want.
  • metric (str, optional) — Only return readings for this one metric name, e.g. "power_draw". Metric names are whatever the device itself reports, there's no fixed list, if you don't know one, call this once without metric and look at what comes back.
  • limit (int, optional) — How many rows to return, from 1 to 1000. Defaults to 100 if omitted.
  • since (int, optional) — Unix milliseconds. Only readings recorded at or after this instant. To get "the last hour," pass int(time.time() * 1000) - 60 * 60 * 1000.

Example

import time

# Last 100 readings, any metric
recent = client.get_telemetry("dev_a1b2c3")

# Last hour of just power_draw
power = client.get_telemetry(
    "dev_a1b2c3",
    metric="power_draw",
    since=int(time.time() * 1000) - 60 * 60 * 1000,
)

Returns List[TelemetryReading]

FieldTypeMeaning
metricstrWhich metric this reading is for.
valuefloat | NoneNumeric reading, e.g. 4.2.
valueTextstr | NoneNon-numeric reading instead, e.g. a status string. Exactly one of value/valueText is set.
unitstr | NoneWhatever unit the device declared, e.g. "watts".
recordedAtstrISO timestamp of when the device reported it.

get_latest_telemetry(device_id)

The device's current state snapshot plus the most recent reading for every metric it reports, in one call, instead of calling get_telemetry per metric and picking off the newest row yourself.

Parameters

  • device_id (str, required) — The device to snapshot.

Example

latest = client.get_latest_telemetry("dev_a1b2c3")
print(latest["relayState"])   # "ON" | "OFF" | None
print(latest["state"])        # whatever free-form state this device reports
print(latest["telemetry"])    # List[TelemetryReading], one per metric, all "latest"

Returns {"state": ..., "relayState": ..., "telemetry": List[TelemetryReading]}state is free-form JSON the device itself reports (shape varies per device), relayState is "ON"/"OFF"/None for a plain relay-style device specifically.

list_sub_devices(device_id)

Physical devices connected locally (not directly to Nexalware) behind this one, if it's acting as a master orchestrating them. Returns an empty list until the master's own firmware actually reports one, this is never populated automatically and never includes software agents, only physical sub-devices the master itself describes.

Parameters

  • device_id (str, required) — The master device's id, not a sub-device id.

Example

subs = client.list_sub_devices("dev_master1")
# [] until the master's firmware calls its own "report sub-device" step

Returns List[SubDevice], each with subDeviceId, externalId (whatever id the master itself uses for it), name, state, capabilities (its own command catalog, if it declared one), isOnline, lastSeen.

get_sub_device(device_id, sub_device_id)

One sub-device's current state and capabilities, a single-item version of list_sub_devices.

Parameters

  • device_id (str, required) — The master device's id.
  • sub_device_id (str, required) — One entry's subDeviceId from a prior list_sub_devices(device_id) call, shaped like "sub_x1y2z3".

Example

sub = client.get_sub_device("dev_master1", "sub_x1y2z3")

Returns SubDevice — same shape as one entry from list_sub_devices.

get_sub_device_telemetry(device_id, sub_device_id, metric=None, limit=None, since=None)

Same idea as get_telemetry, scoped to one sub-device instead of the master itself.

Parameters

  • device_id (str, required) — The master device's id.
  • sub_device_id (str, required) — Which sub-device's history to read.
  • metric / limit / since (optional) — Identical meaning to get_telemetry's same-named arguments.

Example

readings = client.get_sub_device_telemetry("dev_master1", "sub_x1y2z3", limit=50)

Returns List[TelemetryReading] — same shape as get_telemetry's return.

send_sub_device_command(device_id, sub_device_id, cmd, params=None)

The clear-named way to command one sub-device behind a master (equivalent to calling send_command(device_id, cmd, params, target=sub_device_id), use this instead, the argument order reads better). Not validated against a catalog server-side the way send_command is for a top-level device, Nexalware relays it to the master opaquely, and the master and sub-device interpret the command between themselves.

Parameters

  • device_id (str, required) — The master device's id.
  • sub_device_id (str, required) — Which sub-device to target, from list_sub_devices.
  • cmd (str, required) — Whatever command name the sub-device's own declared capabilities.commands says it accepts (check get_sub_device's result), this is the master/sub-device's own vocabulary, not a Nexalware-defined catalog.
  • params (dict, optional) — Arguments for that command, if it needs any, shape is whatever the sub-device itself expects.

Example

client.send_sub_device_command("dev_master1", "sub_x1y2z3", "OPEN")
client.send_sub_device_command("dev_master1", "sub_x1y2z3", "SET_POSITION", params={"angle": 45})

Returns {"ok": True}

list_schedules(device_id)

A device's active schedules, meaning PENDING (not fired yet) or ACTIVE (currently running between its on/off times).

Parameters

  • device_id (str, required) — The device whose schedules to list.

Example

schedules = client.list_schedules("dev_a1b2c3")

Returns List[Schedule], each with scheduleId, slot, onTs, offTs, label, enabled, status, onCommand, offCommand.

get_schedule_context(device_id)

The commands available to schedule for this device, exactly the same catalog get_commands returns, this exists so schedule-building UI code doesn't need to call two different endpoints for the same information.

Parameters

  • device_id (str, required) — The device to check.

Example

context = client.get_schedule_context("dev_a1b2c3")
commands = context["commands"]

Returns {"commands": List[CommandDefinition]} — same CommandDefinition shape documented under get_commands.

create_schedule(device_id, slot, on_ts, off_ts, label=None, enabled=None, on_command=None, off_command=None)

Create or replace one of a device's 5 schedule slots, firing on_command at on_ts and off_command at off_ts. "Replace" means calling this again with the same slot overwrites whatever was there before, it isn't additive.

Parameters

  • device_id (str, required) — The device to schedule.
  • slot (int, required) — Which of the device's 5 schedule slots to use, an integer from 0 to 4. Think of these as 5 fixed "rows" a device has for schedules, not an auto-incrementing list, you pick which row.
  • on_ts (int, required) — Unix seconds (not milliseconds, unlike telemetry's since) for when to fire on_command. To schedule something starting in one hour: int(time.time()) + 3600.
  • off_ts (int, required) — Unix seconds for when to fire off_command. Must make sense relative to on_ts for your use case, the API doesn't force off_ts > on_ts (an overnight schedule legitimately has off_ts "before" on_ts in clock time).
  • label (str, optional) — A short label shown on the dashboard next to this schedule. Max 7 characters - this is a hard limit, e.g. "Evening" doesn't fit but "Eve" does.
  • enabled (bool, optional) — Whether the schedule is active. Omit (None) to default to enabled; pass False to create it disabled (useful for staging a schedule you're not ready to turn on yet).
  • on_command (dict, optional) — Shape: {"command": str, "params": dict} (an Action). Which command fires at on_ts. Omit to default to {"command": "ON"}. command must be a valid cmd from get_commands, exactly like send_command.
  • off_command (dict, optional) — Same shape as on_command, fires at off_ts. Defaults to {"command": "OFF"} if omitted.

Example

import time

# Simplest case: plain ON at 7am, OFF at 10pm, using the defaults
client.create_schedule(
    "dev_a1b2c3",
    slot=0,
    on_ts=int(time.mktime(time.strptime("2026-01-01 07:00:00", "%Y-%m-%d %H:%M:%S"))),
    off_ts=int(time.mktime(time.strptime("2026-01-01 22:00:00", "%Y-%m-%d %H:%M:%S"))),
    label="Day",
)

# Custom commands instead of plain ON/OFF
client.create_schedule(
    "dev_a1b2c3",
    slot=1,
    on_ts=int(time.time()) + 3600,
    off_ts=int(time.time()) + 7200,
    on_command={"command": "SET_BRIGHTNESS", "params": {"level": 80}},
    off_command={"command": "OFF"},
)

Returns Schedule. Raises NexalwareApiError (status 403) if the calling key lacks permission for either command, this is the exact same DeviceGrant check send_command applies, a schedule is just that same action, deferred to a later time.

update_schedule(device_id, slot, on_ts=None, off_ts=None, label=None, enabled=None, on_command=None, off_command=None)

Update an existing schedule slot, only the arguments you actually pass are changed, everything else stays as it was.

Parameters

  • device_id (str, required) — The device whose schedule to update.
  • slot (int, required) — Which slot (0-4) to update, must already exist (created via create_schedule).
  • on_ts / off_ts / label / enabled / on_command / off_command (all optional) — Same meaning as in create_schedule, include only what you're changing.

Example

import time

# Only change the off-time, leave everything else as-is
client.update_schedule("dev_a1b2c3", 0, off_ts=int(time.time()) + 3600)

# Disable it without deleting it
client.update_schedule("dev_a1b2c3", 0, enabled=False)

Returns Schedule

delete_schedule(device_id, slot)

Cancel a schedule slot, freeing it up for a future create_schedule call.

Parameters

  • device_id (str, required) — The device whose schedule to cancel.
  • slot (int, required) — Which slot (0-4) to cancel.

Example

client.delete_schedule("dev_a1b2c3", 0)

Returns {"ok": True}

get_schedule_history(device_id)

A device's completed or cancelled schedules, most recent first, for auditing what actually ran.

Parameters

  • device_id (str, required) — The device to check.

Example

history = client.get_schedule_history("dev_a1b2c3")

Returns List[Schedule] — same Schedule shape as list_schedules, but for slots that are COMPLETED or CANCELLED rather than still pending/active.

Where to go from here

On this page