nexalware 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nexalware-0.1.0/LICENSE +21 -0
- nexalware-0.1.0/PKG-INFO +105 -0
- nexalware-0.1.0/README.md +83 -0
- nexalware-0.1.0/pyproject.toml +37 -0
- nexalware-0.1.0/setup.cfg +4 -0
- nexalware-0.1.0/src/nexalware/__init__.py +39 -0
- nexalware-0.1.0/src/nexalware/client.py +277 -0
- nexalware-0.1.0/src/nexalware/errors.py +24 -0
- nexalware-0.1.0/src/nexalware/py.typed +0 -0
- nexalware-0.1.0/src/nexalware/types.py +101 -0
- nexalware-0.1.0/src/nexalware.egg-info/PKG-INFO +105 -0
- nexalware-0.1.0/src/nexalware.egg-info/SOURCES.txt +12 -0
- nexalware-0.1.0/src/nexalware.egg-info/dependency_links.txt +1 -0
- nexalware-0.1.0/src/nexalware.egg-info/top_level.txt +1 -0
nexalware-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nexalware
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
nexalware-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexalware
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed client for the Nexalware API - control physical devices and read their telemetry from any Python agent or app.
|
|
5
|
+
Author: Nexalware
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://nexalware.com
|
|
8
|
+
Project-URL: Documentation, https://docs.nexalware.com/docs/sdk/sdk-python
|
|
9
|
+
Project-URL: Repository, https://github.com/Darrey1/nexalware-homepage
|
|
10
|
+
Project-URL: Issues, https://github.com/Darrey1/nexalware-homepage/issues
|
|
11
|
+
Keywords: nexalware,iot,device-control,robotics,sdk
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Home Automation
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# nexalware
|
|
24
|
+
|
|
25
|
+
Typed client for the [Nexalware](https://nexalware.com) API, control physical devices and read their telemetry from any Python agent or app. 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.
|
|
26
|
+
|
|
27
|
+
Writing in TypeScript instead? See [`@nexalware/sdk`](https://www.npmjs.com/package/@nexalware/sdk) on npm. Want an MCP-aware host to discover these as tools automatically instead of calling them from code? See [`@nexalware/mcp`](https://www.npmjs.com/package/@nexalware/mcp).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install nexalware
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from nexalware import NexalwareClient
|
|
39
|
+
|
|
40
|
+
client = NexalwareClient(api_key="nxw_live_sk_your_key_here")
|
|
41
|
+
|
|
42
|
+
client.turn_on("dev_a1b2c3")
|
|
43
|
+
|
|
44
|
+
latest = client.get_latest_telemetry("dev_a1b2c3")
|
|
45
|
+
print(latest["state"], latest["telemetry"])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Get an API key from the [dashboard](https://nexalware.com) (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.
|
|
49
|
+
|
|
50
|
+
> **Field names match the API's own JSON, not snake_case.** Return values are plain `dict`s (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.
|
|
51
|
+
|
|
52
|
+
## `NexalwareClient(api_key, base_url=...)`
|
|
53
|
+
|
|
54
|
+
| Param | Type | Required | Meaning |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `api_key` | str | yes | A secret key from the dashboard. |
|
|
57
|
+
| `base_url` | str | no | Override for a self-hosted or staging deployment. Defaults to `https://api.nexalware.com`. |
|
|
58
|
+
| `timeout` | float | no | Abort a request after this many seconds. Defaults to `30.0`. |
|
|
59
|
+
|
|
60
|
+
## Errors
|
|
61
|
+
|
|
62
|
+
Every method raises `NexalwareApiError` on a non-2xx response, it never returns a "silent" error value.
|
|
63
|
+
|
|
64
|
+
| Attribute | Type | Meaning |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `status` | int | HTTP status code. |
|
|
67
|
+
| `error` | str | Short machine-readable code, e.g. `"FORBIDDEN"`, `"NOT_FOUND"`. |
|
|
68
|
+
| `message` | str \| None | Human-readable reason, same text a dashboard user would see. |
|
|
69
|
+
| `details` | list \| None | Only present on a 400 validation failure. |
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from nexalware import NexalwareApiError
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
client.send_command("dev_a1b2c3", "SET_BRIGHTNESS", params={"level": 60})
|
|
76
|
+
except NexalwareApiError as err:
|
|
77
|
+
print(err.status, err.error, err.message)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Methods
|
|
81
|
+
|
|
82
|
+
- `list_devices(project_id=None)` - the devices this key can actually act on, only what its own DeviceGrant(s) cover. Call this first to discover valid `device_id` values instead of needing them hardcoded or pasted in.
|
|
83
|
+
- `get_commands(device_id)` - the command catalog this device accepts.
|
|
84
|
+
- `send_command(device_id, cmd, params=None, target=None)` - send a command to a device, or, with `target` set, to one specific sub-device behind it.
|
|
85
|
+
- `turn_on(device_id)` / `turn_off(device_id)` - shorthand for `send_command(device_id, "ON" | "OFF")`.
|
|
86
|
+
- `get_telemetry(device_id, metric=None, limit=None, since=None)` - historical telemetry readings, newest first.
|
|
87
|
+
- `get_latest_telemetry(device_id)` - current state snapshot plus the most recent reading per metric.
|
|
88
|
+
- `list_sub_devices(device_id)` / `get_sub_device(device_id, sub_device_id)` - physical devices connected locally behind a master device.
|
|
89
|
+
- `get_sub_device_telemetry(device_id, sub_device_id, metric=None, limit=None, since=None)` - same shape as `get_telemetry`, scoped to one sub-device.
|
|
90
|
+
- `send_sub_device_command(device_id, sub_device_id, cmd, params=None)` - convenience wrapper over `send_command` with `target` already set.
|
|
91
|
+
- `list_schedules(device_id)` / `get_schedule_context(device_id)` - a device's active schedules, and the commands available to schedule.
|
|
92
|
+
- `create_schedule(device_id, slot, on_ts, off_ts, ...)` / `update_schedule(...)` / `delete_schedule(device_id, slot)` - manage a device's schedule slots (0-4).
|
|
93
|
+
- `get_schedule_history(device_id)` - a device's completed or cancelled schedules, most recent first.
|
|
94
|
+
|
|
95
|
+
Full parameter/return types for every method: **[SDK Reference](https://docs.nexalware.com/docs/sdk/sdk-python)**.
|
|
96
|
+
|
|
97
|
+
## Links
|
|
98
|
+
|
|
99
|
+
- [Docs](https://docs.nexalware.com)
|
|
100
|
+
- [Device Orchestration](https://docs.nexalware.com/docs/device-orchestration) - sub-devices, and the contract a master implements to report them.
|
|
101
|
+
- [Authentication & Access](https://docs.nexalware.com/docs/concepts/authentication-and-access) - API keys and DeviceGrants.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# nexalware
|
|
2
|
+
|
|
3
|
+
Typed client for the [Nexalware](https://nexalware.com) API, control physical devices and read their telemetry from any Python agent or app. 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.
|
|
4
|
+
|
|
5
|
+
Writing in TypeScript instead? See [`@nexalware/sdk`](https://www.npmjs.com/package/@nexalware/sdk) on npm. Want an MCP-aware host to discover these as tools automatically instead of calling them from code? See [`@nexalware/mcp`](https://www.npmjs.com/package/@nexalware/mcp).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install nexalware
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from nexalware import NexalwareClient
|
|
17
|
+
|
|
18
|
+
client = NexalwareClient(api_key="nxw_live_sk_your_key_here")
|
|
19
|
+
|
|
20
|
+
client.turn_on("dev_a1b2c3")
|
|
21
|
+
|
|
22
|
+
latest = client.get_latest_telemetry("dev_a1b2c3")
|
|
23
|
+
print(latest["state"], latest["telemetry"])
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Get an API key from the [dashboard](https://nexalware.com) (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.
|
|
27
|
+
|
|
28
|
+
> **Field names match the API's own JSON, not snake_case.** Return values are plain `dict`s (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.
|
|
29
|
+
|
|
30
|
+
## `NexalwareClient(api_key, base_url=...)`
|
|
31
|
+
|
|
32
|
+
| Param | Type | Required | Meaning |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| `api_key` | str | yes | A secret key from the dashboard. |
|
|
35
|
+
| `base_url` | str | no | Override for a self-hosted or staging deployment. Defaults to `https://api.nexalware.com`. |
|
|
36
|
+
| `timeout` | float | no | Abort a request after this many seconds. Defaults to `30.0`. |
|
|
37
|
+
|
|
38
|
+
## Errors
|
|
39
|
+
|
|
40
|
+
Every method raises `NexalwareApiError` on a non-2xx response, it never returns a "silent" error value.
|
|
41
|
+
|
|
42
|
+
| Attribute | Type | Meaning |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `status` | int | HTTP status code. |
|
|
45
|
+
| `error` | str | Short machine-readable code, e.g. `"FORBIDDEN"`, `"NOT_FOUND"`. |
|
|
46
|
+
| `message` | str \| None | Human-readable reason, same text a dashboard user would see. |
|
|
47
|
+
| `details` | list \| None | Only present on a 400 validation failure. |
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from nexalware import NexalwareApiError
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
client.send_command("dev_a1b2c3", "SET_BRIGHTNESS", params={"level": 60})
|
|
54
|
+
except NexalwareApiError as err:
|
|
55
|
+
print(err.status, err.error, err.message)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Methods
|
|
59
|
+
|
|
60
|
+
- `list_devices(project_id=None)` - the devices this key can actually act on, only what its own DeviceGrant(s) cover. Call this first to discover valid `device_id` values instead of needing them hardcoded or pasted in.
|
|
61
|
+
- `get_commands(device_id)` - the command catalog this device accepts.
|
|
62
|
+
- `send_command(device_id, cmd, params=None, target=None)` - send a command to a device, or, with `target` set, to one specific sub-device behind it.
|
|
63
|
+
- `turn_on(device_id)` / `turn_off(device_id)` - shorthand for `send_command(device_id, "ON" | "OFF")`.
|
|
64
|
+
- `get_telemetry(device_id, metric=None, limit=None, since=None)` - historical telemetry readings, newest first.
|
|
65
|
+
- `get_latest_telemetry(device_id)` - current state snapshot plus the most recent reading per metric.
|
|
66
|
+
- `list_sub_devices(device_id)` / `get_sub_device(device_id, sub_device_id)` - physical devices connected locally behind a master device.
|
|
67
|
+
- `get_sub_device_telemetry(device_id, sub_device_id, metric=None, limit=None, since=None)` - same shape as `get_telemetry`, scoped to one sub-device.
|
|
68
|
+
- `send_sub_device_command(device_id, sub_device_id, cmd, params=None)` - convenience wrapper over `send_command` with `target` already set.
|
|
69
|
+
- `list_schedules(device_id)` / `get_schedule_context(device_id)` - a device's active schedules, and the commands available to schedule.
|
|
70
|
+
- `create_schedule(device_id, slot, on_ts, off_ts, ...)` / `update_schedule(...)` / `delete_schedule(device_id, slot)` - manage a device's schedule slots (0-4).
|
|
71
|
+
- `get_schedule_history(device_id)` - a device's completed or cancelled schedules, most recent first.
|
|
72
|
+
|
|
73
|
+
Full parameter/return types for every method: **[SDK Reference](https://docs.nexalware.com/docs/sdk/sdk-python)**.
|
|
74
|
+
|
|
75
|
+
## Links
|
|
76
|
+
|
|
77
|
+
- [Docs](https://docs.nexalware.com)
|
|
78
|
+
- [Device Orchestration](https://docs.nexalware.com/docs/device-orchestration) - sub-devices, and the contract a master implements to report them.
|
|
79
|
+
- [Authentication & Access](https://docs.nexalware.com/docs/concepts/authentication-and-access) - API keys and DeviceGrants.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nexalware"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Typed client for the Nexalware API - control physical devices and read their telemetry from any Python agent or app."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Nexalware" }]
|
|
13
|
+
keywords = ["nexalware", "iot", "device-control", "robotics", "sdk"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
"Topic :: Home Automation",
|
|
21
|
+
]
|
|
22
|
+
# Deliberately zero third-party dependencies, built on urllib from the
|
|
23
|
+
# standard library, so dropping this into any existing agent environment
|
|
24
|
+
# can never trigger a dependency conflict.
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://nexalware.com"
|
|
29
|
+
Documentation = "https://docs.nexalware.com/docs/sdk/sdk-python"
|
|
30
|
+
Repository = "https://github.com/Darrey1/nexalware-homepage"
|
|
31
|
+
Issues = "https://github.com/Darrey1/nexalware-homepage/issues"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
nexalware = ["py.typed"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from .client import DEFAULT_BASE_URL, NexalwareClient
|
|
4
|
+
from .errors import NexalwareApiError
|
|
5
|
+
from .types import (
|
|
6
|
+
Action,
|
|
7
|
+
CommandDefinition,
|
|
8
|
+
Device,
|
|
9
|
+
LatestTelemetry,
|
|
10
|
+
Schedule,
|
|
11
|
+
SendCommandResult,
|
|
12
|
+
SubDevice,
|
|
13
|
+
SubDeviceCapabilities,
|
|
14
|
+
TelemetryReading,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"NexalwareClient",
|
|
19
|
+
"NexalwareApiError",
|
|
20
|
+
"DEFAULT_BASE_URL",
|
|
21
|
+
"Action",
|
|
22
|
+
"CommandDefinition",
|
|
23
|
+
"Device",
|
|
24
|
+
"LatestTelemetry",
|
|
25
|
+
"Schedule",
|
|
26
|
+
"SendCommandResult",
|
|
27
|
+
"SubDevice",
|
|
28
|
+
"SubDeviceCapabilities",
|
|
29
|
+
"TelemetryReading",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
# Single source of truth is pyproject.toml's version - read it back at
|
|
34
|
+
# runtime instead of hand-duplicating it here, so the two can never
|
|
35
|
+
# drift out of sync.
|
|
36
|
+
__version__ = version("nexalware")
|
|
37
|
+
except PackageNotFoundError:
|
|
38
|
+
# Editable/local checkout with no installed distribution metadata yet.
|
|
39
|
+
__version__ = "0.0.0+unknown"
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import socket
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import urllib.request
|
|
8
|
+
from typing import Any, Dict, List, Optional, cast
|
|
9
|
+
|
|
10
|
+
from .errors import NexalwareApiError
|
|
11
|
+
from .types import (
|
|
12
|
+
Action,
|
|
13
|
+
CommandDefinition,
|
|
14
|
+
Device,
|
|
15
|
+
LatestTelemetry,
|
|
16
|
+
Schedule,
|
|
17
|
+
SendCommandResult,
|
|
18
|
+
SubDevice,
|
|
19
|
+
TelemetryReading,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
DEFAULT_BASE_URL = "https://api.nexalware.com"
|
|
23
|
+
DEFAULT_TIMEOUT_SECONDS = 30.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NexalwareClient:
|
|
27
|
+
"""Thin typed wrapper over the Nexalware "Device Control" API tier, the
|
|
28
|
+
same endpoints an API key can already call today. Built on ``urllib``
|
|
29
|
+
from the standard library only, no third-party dependency, so dropping
|
|
30
|
+
this into any existing agent environment can never trigger a version
|
|
31
|
+
conflict.
|
|
32
|
+
|
|
33
|
+
A key's actual reach is decided entirely by its DeviceGrant(s) on the
|
|
34
|
+
dashboard, this client never expands or assumes permissions beyond that.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
api_key: str,
|
|
40
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
41
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
42
|
+
) -> None:
|
|
43
|
+
if not api_key:
|
|
44
|
+
raise ValueError("NexalwareClient requires an api_key")
|
|
45
|
+
self._api_key = api_key
|
|
46
|
+
self._base_url = base_url.rstrip("/")
|
|
47
|
+
self._timeout = timeout
|
|
48
|
+
|
|
49
|
+
def _request(
|
|
50
|
+
self,
|
|
51
|
+
method: str,
|
|
52
|
+
path: str,
|
|
53
|
+
body: Optional[Dict[str, Any]] = None,
|
|
54
|
+
query: Optional[Dict[str, Any]] = None,
|
|
55
|
+
) -> Any:
|
|
56
|
+
url = f"{self._base_url}/api/v1{path}"
|
|
57
|
+
if query:
|
|
58
|
+
filtered = {k: v for k, v in query.items() if v is not None}
|
|
59
|
+
if filtered:
|
|
60
|
+
url += "?" + urllib.parse.urlencode(filtered)
|
|
61
|
+
|
|
62
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
63
|
+
request = urllib.request.Request(
|
|
64
|
+
url,
|
|
65
|
+
data=data,
|
|
66
|
+
method=method,
|
|
67
|
+
headers={
|
|
68
|
+
"X-Api-Key": self._api_key,
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"User-Agent": "nexalware-sdk-python",
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
try:
|
|
74
|
+
with urllib.request.urlopen(request, timeout=self._timeout) as response:
|
|
75
|
+
text = response.read().decode("utf-8")
|
|
76
|
+
if not text:
|
|
77
|
+
return None
|
|
78
|
+
try:
|
|
79
|
+
return json.loads(text)
|
|
80
|
+
except ValueError:
|
|
81
|
+
raise NexalwareApiError(
|
|
82
|
+
status=response.status,
|
|
83
|
+
error="INVALID_RESPONSE",
|
|
84
|
+
message=f"Nexalware API returned a non-JSON response from {path}",
|
|
85
|
+
) from None
|
|
86
|
+
except urllib.error.HTTPError as err:
|
|
87
|
+
text = err.read().decode("utf-8")
|
|
88
|
+
# The API always returns JSON, but a proxy/CDN error page in
|
|
89
|
+
# front of it (502, WAF block) won't - fail with a clear
|
|
90
|
+
# NexalwareApiError either way, not an unhandled ValueError.
|
|
91
|
+
try:
|
|
92
|
+
payload = json.loads(text) if text else {}
|
|
93
|
+
except ValueError:
|
|
94
|
+
payload = {"message": text[:200]} if text else {}
|
|
95
|
+
raise NexalwareApiError(
|
|
96
|
+
status=err.code,
|
|
97
|
+
error=payload.get("error", "UNKNOWN_ERROR"),
|
|
98
|
+
message=payload.get("message"),
|
|
99
|
+
details=payload.get("details"),
|
|
100
|
+
) from None
|
|
101
|
+
except socket.timeout:
|
|
102
|
+
# socket.timeout is TimeoutError itself on 3.10+, but stays a
|
|
103
|
+
# distinct class pre-3.10 (this package supports 3.9+), so
|
|
104
|
+
# catch it by its always-correct name rather than TimeoutError.
|
|
105
|
+
raise TimeoutError(f"Nexalware API request to {path} timed out after {self._timeout}s") from None
|
|
106
|
+
|
|
107
|
+
def list_devices(self, project_id: Optional[str] = None) -> List[Device]:
|
|
108
|
+
"""The devices this key can actually act on - only what its own
|
|
109
|
+
DeviceGrant(s) cover, never the rest of the account. Call this
|
|
110
|
+
first to discover valid device_id values instead of needing them
|
|
111
|
+
hardcoded or pasted in from the dashboard.
|
|
112
|
+
"""
|
|
113
|
+
return cast(List[Device], self._request("GET", "/devices", query={"projectId": project_id}))
|
|
114
|
+
|
|
115
|
+
def get_commands(self, device_id: str) -> List[CommandDefinition]:
|
|
116
|
+
"""The command catalog this device accepts."""
|
|
117
|
+
return cast(List[CommandDefinition], self._request("GET", f"/devices/{device_id}/commands"))
|
|
118
|
+
|
|
119
|
+
def send_command(
|
|
120
|
+
self,
|
|
121
|
+
device_id: str,
|
|
122
|
+
cmd: str,
|
|
123
|
+
params: Optional[Dict[str, Any]] = None,
|
|
124
|
+
target: Optional[str] = None,
|
|
125
|
+
) -> SendCommandResult:
|
|
126
|
+
"""Send a command to a device, or, with ``target`` set, to one
|
|
127
|
+
specific sub-device behind it (see ``send_sub_device_command`` for
|
|
128
|
+
the convenience form). Raises ``NexalwareApiError`` (status 403) if
|
|
129
|
+
the calling key has no DeviceGrant covering this command.
|
|
130
|
+
"""
|
|
131
|
+
body: Dict[str, Any] = {"cmd": cmd}
|
|
132
|
+
if params is not None:
|
|
133
|
+
body["params"] = params
|
|
134
|
+
if target is not None:
|
|
135
|
+
body["target"] = target
|
|
136
|
+
return cast(SendCommandResult, self._request("POST", f"/devices/{device_id}/command", body=body))
|
|
137
|
+
|
|
138
|
+
def turn_on(self, device_id: str) -> SendCommandResult:
|
|
139
|
+
"""Shorthand for ``send_command(device_id, "ON")``."""
|
|
140
|
+
return cast(SendCommandResult, self._request("POST", f"/devices/{device_id}/command/on"))
|
|
141
|
+
|
|
142
|
+
def turn_off(self, device_id: str) -> SendCommandResult:
|
|
143
|
+
"""Shorthand for ``send_command(device_id, "OFF")``."""
|
|
144
|
+
return cast(SendCommandResult, self._request("POST", f"/devices/{device_id}/command/off"))
|
|
145
|
+
|
|
146
|
+
def get_telemetry(
|
|
147
|
+
self,
|
|
148
|
+
device_id: str,
|
|
149
|
+
metric: Optional[str] = None,
|
|
150
|
+
limit: Optional[int] = None,
|
|
151
|
+
since: Optional[int] = None,
|
|
152
|
+
) -> List[TelemetryReading]:
|
|
153
|
+
"""Historical telemetry readings, newest first."""
|
|
154
|
+
return cast(
|
|
155
|
+
List[TelemetryReading],
|
|
156
|
+
self._request(
|
|
157
|
+
"GET",
|
|
158
|
+
f"/devices/{device_id}/telemetry",
|
|
159
|
+
query={"metric": metric, "limit": limit, "since": since},
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def get_latest_telemetry(self, device_id: str) -> LatestTelemetry:
|
|
164
|
+
"""The device's current state plus the most recent reading per metric."""
|
|
165
|
+
return cast(LatestTelemetry, self._request("GET", f"/devices/{device_id}/telemetry/latest"))
|
|
166
|
+
|
|
167
|
+
def list_sub_devices(self, device_id: str) -> List[SubDevice]:
|
|
168
|
+
"""Physical devices connected locally behind this one, acting as an
|
|
169
|
+
orchestrator ("master"). Empty until the master actually reports one.
|
|
170
|
+
"""
|
|
171
|
+
return cast(List[SubDevice], self._request("GET", f"/devices/{device_id}/sub-devices"))
|
|
172
|
+
|
|
173
|
+
def get_sub_device(self, device_id: str, sub_device_id: str) -> SubDevice:
|
|
174
|
+
return cast(SubDevice, self._request("GET", f"/devices/{device_id}/sub-devices/{sub_device_id}"))
|
|
175
|
+
|
|
176
|
+
def get_sub_device_telemetry(
|
|
177
|
+
self,
|
|
178
|
+
device_id: str,
|
|
179
|
+
sub_device_id: str,
|
|
180
|
+
metric: Optional[str] = None,
|
|
181
|
+
limit: Optional[int] = None,
|
|
182
|
+
since: Optional[int] = None,
|
|
183
|
+
) -> List[TelemetryReading]:
|
|
184
|
+
return cast(
|
|
185
|
+
List[TelemetryReading],
|
|
186
|
+
self._request(
|
|
187
|
+
"GET",
|
|
188
|
+
f"/devices/{device_id}/sub-devices/{sub_device_id}/telemetry",
|
|
189
|
+
query={"metric": metric, "limit": limit, "since": since},
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def send_sub_device_command(
|
|
194
|
+
self,
|
|
195
|
+
device_id: str,
|
|
196
|
+
sub_device_id: str,
|
|
197
|
+
cmd: str,
|
|
198
|
+
params: Optional[Dict[str, Any]] = None,
|
|
199
|
+
) -> SendCommandResult:
|
|
200
|
+
"""Convenience wrapper over ``send_command`` with ``target`` set to a sub-device."""
|
|
201
|
+
return self.send_command(device_id, cmd, params, target=sub_device_id)
|
|
202
|
+
|
|
203
|
+
def list_schedules(self, device_id: str) -> List[Schedule]:
|
|
204
|
+
"""A device's active (pending or currently running) schedules."""
|
|
205
|
+
return cast(List[Schedule], self._request("GET", f"/devices/{device_id}/schedules"))
|
|
206
|
+
|
|
207
|
+
def get_schedule_context(self, device_id: str) -> Dict[str, List[CommandDefinition]]:
|
|
208
|
+
"""The commands available to schedule, same catalog ``get_commands`` returns."""
|
|
209
|
+
return cast(
|
|
210
|
+
Dict[str, List[CommandDefinition]],
|
|
211
|
+
self._request("GET", f"/devices/{device_id}/schedules/context"),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def create_schedule(
|
|
215
|
+
self,
|
|
216
|
+
device_id: str,
|
|
217
|
+
slot: int,
|
|
218
|
+
on_ts: int,
|
|
219
|
+
off_ts: int,
|
|
220
|
+
label: Optional[str] = None,
|
|
221
|
+
enabled: Optional[bool] = None,
|
|
222
|
+
on_command: Optional[Action] = None,
|
|
223
|
+
off_command: Optional[Action] = None,
|
|
224
|
+
) -> Schedule:
|
|
225
|
+
"""Create or replace one of a device's schedule slots (0-4), firing
|
|
226
|
+
``on_command`` at ``on_ts`` and ``off_command`` at ``off_ts``.
|
|
227
|
+
``on_command``/``off_command`` default to plain ON/OFF, any other
|
|
228
|
+
command still fires reliably from the server at the scheduled time,
|
|
229
|
+
but only the plain ON/OFF case also syncs to firmware for offline
|
|
230
|
+
self-execution. Raises ``NexalwareApiError`` (status 403) if the
|
|
231
|
+
calling key has no permission for either command.
|
|
232
|
+
"""
|
|
233
|
+
body: Dict[str, Any] = {"slot": slot, "onTs": on_ts, "offTs": off_ts}
|
|
234
|
+
if label is not None:
|
|
235
|
+
body["label"] = label
|
|
236
|
+
if enabled is not None:
|
|
237
|
+
body["enabled"] = enabled
|
|
238
|
+
if on_command is not None:
|
|
239
|
+
body["onCommand"] = on_command
|
|
240
|
+
if off_command is not None:
|
|
241
|
+
body["offCommand"] = off_command
|
|
242
|
+
return cast(Schedule, self._request("POST", f"/devices/{device_id}/schedules", body=body))
|
|
243
|
+
|
|
244
|
+
def update_schedule(
|
|
245
|
+
self,
|
|
246
|
+
device_id: str,
|
|
247
|
+
slot: int,
|
|
248
|
+
on_ts: Optional[int] = None,
|
|
249
|
+
off_ts: Optional[int] = None,
|
|
250
|
+
label: Optional[str] = None,
|
|
251
|
+
enabled: Optional[bool] = None,
|
|
252
|
+
on_command: Optional[Action] = None,
|
|
253
|
+
off_command: Optional[Action] = None,
|
|
254
|
+
) -> Schedule:
|
|
255
|
+
"""Update an existing schedule slot, only the fields provided are changed."""
|
|
256
|
+
body: Dict[str, Any] = {}
|
|
257
|
+
if on_ts is not None:
|
|
258
|
+
body["onTs"] = on_ts
|
|
259
|
+
if off_ts is not None:
|
|
260
|
+
body["offTs"] = off_ts
|
|
261
|
+
if label is not None:
|
|
262
|
+
body["label"] = label
|
|
263
|
+
if enabled is not None:
|
|
264
|
+
body["enabled"] = enabled
|
|
265
|
+
if on_command is not None:
|
|
266
|
+
body["onCommand"] = on_command
|
|
267
|
+
if off_command is not None:
|
|
268
|
+
body["offCommand"] = off_command
|
|
269
|
+
return cast(Schedule, self._request("PUT", f"/devices/{device_id}/schedules/{slot}", body=body))
|
|
270
|
+
|
|
271
|
+
def delete_schedule(self, device_id: str, slot: int) -> Dict[str, bool]:
|
|
272
|
+
"""Cancel a schedule slot."""
|
|
273
|
+
return cast(Dict[str, bool], self._request("DELETE", f"/devices/{device_id}/schedules/{slot}"))
|
|
274
|
+
|
|
275
|
+
def get_schedule_history(self, device_id: str) -> List[Schedule]:
|
|
276
|
+
"""A device's completed or cancelled schedules, most recent first."""
|
|
277
|
+
return cast(List[Schedule], self._request("GET", f"/devices/{device_id}/schedules/history"))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NexalwareApiError(Exception):
|
|
7
|
+
"""Raised for any non-2xx response from the Nexalware API. Mirrors the
|
|
8
|
+
platform's own error envelope (``{ error, message?, details? }``) so
|
|
9
|
+
callers get the same reason a dashboard user or curl call would see,
|
|
10
|
+
not a generic HTTP failure.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
status: int,
|
|
16
|
+
error: str,
|
|
17
|
+
message: Optional[str] = None,
|
|
18
|
+
details: Optional[List[Any]] = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
self.status = status
|
|
21
|
+
self.error = error
|
|
22
|
+
self.message = message
|
|
23
|
+
self.details = details
|
|
24
|
+
super().__init__(message or error or f"Nexalware API request failed with status {status}")
|
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Literal, Optional, TypedDict
|
|
4
|
+
|
|
5
|
+
# Field names deliberately match the API's own JSON wire format (camelCase)
|
|
6
|
+
# rather than being renamed to snake_case, so an example or error message
|
|
7
|
+
# from the docs, the REST API, or this SDK all refer to the same field by
|
|
8
|
+
# the same name, nothing to mentally translate between them.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DeviceProject(TypedDict):
|
|
12
|
+
name: str
|
|
13
|
+
projectId: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Device(TypedDict):
|
|
17
|
+
deviceId: str
|
|
18
|
+
name: str
|
|
19
|
+
boardType: str
|
|
20
|
+
appType: str
|
|
21
|
+
deviceStatus: Literal["PENDING", "ACTIVE", "DISABLED", "REVOKED"]
|
|
22
|
+
enabled: bool
|
|
23
|
+
deviceTypeId: Optional[str]
|
|
24
|
+
relayState: Optional[bool]
|
|
25
|
+
isOnline: bool
|
|
26
|
+
lastSeen: Optional[str]
|
|
27
|
+
createdAt: str
|
|
28
|
+
project: Optional[DeviceProject]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CommandDefinition(TypedDict, total=False):
|
|
32
|
+
commandId: str
|
|
33
|
+
name: str
|
|
34
|
+
label: str
|
|
35
|
+
description: str
|
|
36
|
+
kind: Literal["ACTION", "QUERY"]
|
|
37
|
+
paramsSchema: Optional[Dict[str, Any]]
|
|
38
|
+
shape: Optional[Dict[str, Any]]
|
|
39
|
+
requiresApproval: bool
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SendCommandResult(TypedDict, total=False):
|
|
43
|
+
ok: Literal[True]
|
|
44
|
+
approvalRequired: bool
|
|
45
|
+
approvalId: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TelemetryReading(TypedDict):
|
|
49
|
+
id: str
|
|
50
|
+
deviceId: str
|
|
51
|
+
accountId: str
|
|
52
|
+
subDeviceId: Optional[str]
|
|
53
|
+
metric: str
|
|
54
|
+
value: Optional[float]
|
|
55
|
+
valueText: Optional[str]
|
|
56
|
+
unit: Optional[str]
|
|
57
|
+
raw: Any
|
|
58
|
+
recordedAt: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LatestTelemetry(TypedDict):
|
|
62
|
+
state: Optional[Dict[str, Any]]
|
|
63
|
+
relayState: Optional[Literal["ON", "OFF"]]
|
|
64
|
+
telemetry: List[TelemetryReading]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SubDeviceCapabilities(TypedDict, total=False):
|
|
68
|
+
commands: List[Dict[str, Any]]
|
|
69
|
+
telemetry_metrics: List[str]
|
|
70
|
+
verifiable: bool
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SubDevice(TypedDict):
|
|
74
|
+
subDeviceId: str
|
|
75
|
+
externalId: str
|
|
76
|
+
name: str
|
|
77
|
+
state: Optional[Dict[str, Any]]
|
|
78
|
+
capabilities: Optional[SubDeviceCapabilities]
|
|
79
|
+
verifiable: bool
|
|
80
|
+
enabled: bool
|
|
81
|
+
isOnline: bool
|
|
82
|
+
lastSeen: Optional[str]
|
|
83
|
+
createdAt: str
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Action(TypedDict, total=False):
|
|
87
|
+
command: str
|
|
88
|
+
params: Dict[str, Any]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Schedule(TypedDict):
|
|
92
|
+
scheduleId: str
|
|
93
|
+
deviceId: str
|
|
94
|
+
slot: int
|
|
95
|
+
onTs: int
|
|
96
|
+
offTs: int
|
|
97
|
+
label: str
|
|
98
|
+
enabled: bool
|
|
99
|
+
status: Literal["PENDING", "ACTIVE", "COMPLETED", "CANCELLED"]
|
|
100
|
+
onCommand: Action
|
|
101
|
+
offCommand: Action
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexalware
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed client for the Nexalware API - control physical devices and read their telemetry from any Python agent or app.
|
|
5
|
+
Author: Nexalware
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://nexalware.com
|
|
8
|
+
Project-URL: Documentation, https://docs.nexalware.com/docs/sdk/sdk-python
|
|
9
|
+
Project-URL: Repository, https://github.com/Darrey1/nexalware-homepage
|
|
10
|
+
Project-URL: Issues, https://github.com/Darrey1/nexalware-homepage/issues
|
|
11
|
+
Keywords: nexalware,iot,device-control,robotics,sdk
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Home Automation
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# nexalware
|
|
24
|
+
|
|
25
|
+
Typed client for the [Nexalware](https://nexalware.com) API, control physical devices and read their telemetry from any Python agent or app. 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.
|
|
26
|
+
|
|
27
|
+
Writing in TypeScript instead? See [`@nexalware/sdk`](https://www.npmjs.com/package/@nexalware/sdk) on npm. Want an MCP-aware host to discover these as tools automatically instead of calling them from code? See [`@nexalware/mcp`](https://www.npmjs.com/package/@nexalware/mcp).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install nexalware
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from nexalware import NexalwareClient
|
|
39
|
+
|
|
40
|
+
client = NexalwareClient(api_key="nxw_live_sk_your_key_here")
|
|
41
|
+
|
|
42
|
+
client.turn_on("dev_a1b2c3")
|
|
43
|
+
|
|
44
|
+
latest = client.get_latest_telemetry("dev_a1b2c3")
|
|
45
|
+
print(latest["state"], latest["telemetry"])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Get an API key from the [dashboard](https://nexalware.com) (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.
|
|
49
|
+
|
|
50
|
+
> **Field names match the API's own JSON, not snake_case.** Return values are plain `dict`s (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.
|
|
51
|
+
|
|
52
|
+
## `NexalwareClient(api_key, base_url=...)`
|
|
53
|
+
|
|
54
|
+
| Param | Type | Required | Meaning |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `api_key` | str | yes | A secret key from the dashboard. |
|
|
57
|
+
| `base_url` | str | no | Override for a self-hosted or staging deployment. Defaults to `https://api.nexalware.com`. |
|
|
58
|
+
| `timeout` | float | no | Abort a request after this many seconds. Defaults to `30.0`. |
|
|
59
|
+
|
|
60
|
+
## Errors
|
|
61
|
+
|
|
62
|
+
Every method raises `NexalwareApiError` on a non-2xx response, it never returns a "silent" error value.
|
|
63
|
+
|
|
64
|
+
| Attribute | Type | Meaning |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `status` | int | HTTP status code. |
|
|
67
|
+
| `error` | str | Short machine-readable code, e.g. `"FORBIDDEN"`, `"NOT_FOUND"`. |
|
|
68
|
+
| `message` | str \| None | Human-readable reason, same text a dashboard user would see. |
|
|
69
|
+
| `details` | list \| None | Only present on a 400 validation failure. |
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from nexalware import NexalwareApiError
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
client.send_command("dev_a1b2c3", "SET_BRIGHTNESS", params={"level": 60})
|
|
76
|
+
except NexalwareApiError as err:
|
|
77
|
+
print(err.status, err.error, err.message)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Methods
|
|
81
|
+
|
|
82
|
+
- `list_devices(project_id=None)` - the devices this key can actually act on, only what its own DeviceGrant(s) cover. Call this first to discover valid `device_id` values instead of needing them hardcoded or pasted in.
|
|
83
|
+
- `get_commands(device_id)` - the command catalog this device accepts.
|
|
84
|
+
- `send_command(device_id, cmd, params=None, target=None)` - send a command to a device, or, with `target` set, to one specific sub-device behind it.
|
|
85
|
+
- `turn_on(device_id)` / `turn_off(device_id)` - shorthand for `send_command(device_id, "ON" | "OFF")`.
|
|
86
|
+
- `get_telemetry(device_id, metric=None, limit=None, since=None)` - historical telemetry readings, newest first.
|
|
87
|
+
- `get_latest_telemetry(device_id)` - current state snapshot plus the most recent reading per metric.
|
|
88
|
+
- `list_sub_devices(device_id)` / `get_sub_device(device_id, sub_device_id)` - physical devices connected locally behind a master device.
|
|
89
|
+
- `get_sub_device_telemetry(device_id, sub_device_id, metric=None, limit=None, since=None)` - same shape as `get_telemetry`, scoped to one sub-device.
|
|
90
|
+
- `send_sub_device_command(device_id, sub_device_id, cmd, params=None)` - convenience wrapper over `send_command` with `target` already set.
|
|
91
|
+
- `list_schedules(device_id)` / `get_schedule_context(device_id)` - a device's active schedules, and the commands available to schedule.
|
|
92
|
+
- `create_schedule(device_id, slot, on_ts, off_ts, ...)` / `update_schedule(...)` / `delete_schedule(device_id, slot)` - manage a device's schedule slots (0-4).
|
|
93
|
+
- `get_schedule_history(device_id)` - a device's completed or cancelled schedules, most recent first.
|
|
94
|
+
|
|
95
|
+
Full parameter/return types for every method: **[SDK Reference](https://docs.nexalware.com/docs/sdk/sdk-python)**.
|
|
96
|
+
|
|
97
|
+
## Links
|
|
98
|
+
|
|
99
|
+
- [Docs](https://docs.nexalware.com)
|
|
100
|
+
- [Device Orchestration](https://docs.nexalware.com/docs/device-orchestration) - sub-devices, and the contract a master implements to report them.
|
|
101
|
+
- [Authentication & Access](https://docs.nexalware.com/docs/concepts/authentication-and-access) - API keys and DeviceGrants.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/nexalware/__init__.py
|
|
5
|
+
src/nexalware/client.py
|
|
6
|
+
src/nexalware/errors.py
|
|
7
|
+
src/nexalware/py.typed
|
|
8
|
+
src/nexalware/types.py
|
|
9
|
+
src/nexalware.egg-info/PKG-INFO
|
|
10
|
+
src/nexalware.egg-info/SOURCES.txt
|
|
11
|
+
src/nexalware.egg-info/dependency_links.txt
|
|
12
|
+
src/nexalware.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nexalware
|