toybox-3d-api 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.
- toybox_3d_api-0.1.0/LICENSE +21 -0
- toybox_3d_api-0.1.0/PKG-INFO +92 -0
- toybox_3d_api-0.1.0/README.md +74 -0
- toybox_3d_api-0.1.0/pyproject.toml +28 -0
- toybox_3d_api-0.1.0/setup.cfg +4 -0
- toybox_3d_api-0.1.0/tests/test_connection.py +16 -0
- toybox_3d_api-0.1.0/tests/test_dump.py +48 -0
- toybox_3d_api-0.1.0/tests/test_live.py +168 -0
- toybox_3d_api-0.1.0/toybox_3d_api.egg-info/PKG-INFO +92 -0
- toybox_3d_api-0.1.0/toybox_3d_api.egg-info/SOURCES.txt +16 -0
- toybox_3d_api-0.1.0/toybox_3d_api.egg-info/dependency_links.txt +1 -0
- toybox_3d_api-0.1.0/toybox_3d_api.egg-info/requires.txt +5 -0
- toybox_3d_api-0.1.0/toybox_3d_api.egg-info/top_level.txt +1 -0
- toybox_3d_api-0.1.0/toybox_api/__init__.py +33 -0
- toybox_3d_api-0.1.0/toybox_api/client.py +601 -0
- toybox_3d_api-0.1.0/toybox_api/const.py +25 -0
- toybox_3d_api-0.1.0/toybox_api/exceptions.py +21 -0
- toybox_3d_api-0.1.0/toybox_api/models.py +387 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sgarrity
|
|
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.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: toybox-3d-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python API client for ToyBox 3D printers (make.toys) via Meteor DDP
|
|
5
|
+
Author: stgarrity
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/stgarrity/toybox-api
|
|
8
|
+
Project-URL: Repository, https://github.com/stgarrity/toybox-api
|
|
9
|
+
Project-URL: Issues, https://github.com/stgarrity/toybox-api/issues
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# toybox-api — Python Client for ToyBox 3D Printers
|
|
20
|
+
|
|
21
|
+
Async Python client for [ToyBox 3D printers](https://www.toybox.com/) via the [make.toys](https://www.make.toys) Meteor DDP protocol.
|
|
22
|
+
|
|
23
|
+
Used by the [Home Assistant integration](https://github.com/stgarrity/homeassistant-toybox).
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- Meteor DDP WebSocket client (no REST — make.toys is DDP-only)
|
|
28
|
+
- Authentication via Meteor accounts
|
|
29
|
+
- Real-time printer state via DDP subscriptions (`printerStates` collection)
|
|
30
|
+
- Print job tracking via `toyPrints` collection
|
|
31
|
+
- Time remaining / elapsed / progress calculations matching make.toys web app logic
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install toybox-api
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import asyncio
|
|
43
|
+
from toybox_api import ToyBoxClient
|
|
44
|
+
|
|
45
|
+
async def main():
|
|
46
|
+
async with ToyBoxClient() as client:
|
|
47
|
+
await client.connect()
|
|
48
|
+
await client.authenticate("email@example.com", "password")
|
|
49
|
+
await client.subscribe_to_printer_data(["printer_id"])
|
|
50
|
+
|
|
51
|
+
data = await client.get_all_data()
|
|
52
|
+
print(f"Printer: {data.printer.display_name}")
|
|
53
|
+
print(f"Online: {data.printer.is_online}")
|
|
54
|
+
print(f"State: {data.print_state}")
|
|
55
|
+
|
|
56
|
+
if data.current_request:
|
|
57
|
+
print(f"Printing: {data.current_request.print_name}")
|
|
58
|
+
print(f"Remaining: {data.current_request.remaining_seconds}s")
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Data Model
|
|
64
|
+
|
|
65
|
+
| Class | Source | Description |
|
|
66
|
+
|-------|--------|-------------|
|
|
67
|
+
| `PrinterStatus` | `printerStates` collection | Online state, model, hardware ID, firmware |
|
|
68
|
+
| `PrintRequest` | `toyPrints` collection | Active/completed prints with timing data |
|
|
69
|
+
| `ToyBoxData` | Coordinator container | Combines printer + current/last print request |
|
|
70
|
+
|
|
71
|
+
## Status
|
|
72
|
+
|
|
73
|
+
🟡 **Beta** — Implements real Meteor DDP protocol. Needs live testing with active prints.
|
|
74
|
+
|
|
75
|
+
## Project Structure
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
ha-toybox/
|
|
79
|
+
├── toybox_api/
|
|
80
|
+
│ ├── __init__.py
|
|
81
|
+
│ ├── client.py # DDP WebSocket client
|
|
82
|
+
│ ├── models.py # PrinterStatus, PrintRequest, ToyBoxData
|
|
83
|
+
│ ├── exceptions.py # ToyBoxError hierarchy
|
|
84
|
+
│ └── const.py # DDP URLs, subscription/method names
|
|
85
|
+
├── pyproject.toml
|
|
86
|
+
├── LICENSE
|
|
87
|
+
└── README.md
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# toybox-api — Python Client for ToyBox 3D Printers
|
|
2
|
+
|
|
3
|
+
Async Python client for [ToyBox 3D printers](https://www.toybox.com/) via the [make.toys](https://www.make.toys) Meteor DDP protocol.
|
|
4
|
+
|
|
5
|
+
Used by the [Home Assistant integration](https://github.com/stgarrity/homeassistant-toybox).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Meteor DDP WebSocket client (no REST — make.toys is DDP-only)
|
|
10
|
+
- Authentication via Meteor accounts
|
|
11
|
+
- Real-time printer state via DDP subscriptions (`printerStates` collection)
|
|
12
|
+
- Print job tracking via `toyPrints` collection
|
|
13
|
+
- Time remaining / elapsed / progress calculations matching make.toys web app logic
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install toybox-api
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import asyncio
|
|
25
|
+
from toybox_api import ToyBoxClient
|
|
26
|
+
|
|
27
|
+
async def main():
|
|
28
|
+
async with ToyBoxClient() as client:
|
|
29
|
+
await client.connect()
|
|
30
|
+
await client.authenticate("email@example.com", "password")
|
|
31
|
+
await client.subscribe_to_printer_data(["printer_id"])
|
|
32
|
+
|
|
33
|
+
data = await client.get_all_data()
|
|
34
|
+
print(f"Printer: {data.printer.display_name}")
|
|
35
|
+
print(f"Online: {data.printer.is_online}")
|
|
36
|
+
print(f"State: {data.print_state}")
|
|
37
|
+
|
|
38
|
+
if data.current_request:
|
|
39
|
+
print(f"Printing: {data.current_request.print_name}")
|
|
40
|
+
print(f"Remaining: {data.current_request.remaining_seconds}s")
|
|
41
|
+
|
|
42
|
+
asyncio.run(main())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Data Model
|
|
46
|
+
|
|
47
|
+
| Class | Source | Description |
|
|
48
|
+
|-------|--------|-------------|
|
|
49
|
+
| `PrinterStatus` | `printerStates` collection | Online state, model, hardware ID, firmware |
|
|
50
|
+
| `PrintRequest` | `toyPrints` collection | Active/completed prints with timing data |
|
|
51
|
+
| `ToyBoxData` | Coordinator container | Combines printer + current/last print request |
|
|
52
|
+
|
|
53
|
+
## Status
|
|
54
|
+
|
|
55
|
+
🟡 **Beta** — Implements real Meteor DDP protocol. Needs live testing with active prints.
|
|
56
|
+
|
|
57
|
+
## Project Structure
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
ha-toybox/
|
|
61
|
+
├── toybox_api/
|
|
62
|
+
│ ├── __init__.py
|
|
63
|
+
│ ├── client.py # DDP WebSocket client
|
|
64
|
+
│ ├── models.py # PrinterStatus, PrintRequest, ToyBoxData
|
|
65
|
+
│ ├── exceptions.py # ToyBoxError hierarchy
|
|
66
|
+
│ └── const.py # DDP URLs, subscription/method names
|
|
67
|
+
├── pyproject.toml
|
|
68
|
+
├── LICENSE
|
|
69
|
+
└── README.md
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=45", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "toybox-3d-api"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python API client for ToyBox 3D printers (make.toys) via Meteor DDP"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "stgarrity"}
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"aiohttp>=3.9.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=7.0",
|
|
22
|
+
"pytest-asyncio>=0.21.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/stgarrity/toybox-api"
|
|
27
|
+
Repository = "https://github.com/stgarrity/toybox-api"
|
|
28
|
+
Issues = "https://github.com/stgarrity/toybox-api/issues"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Minimal connection test - no credentials needed."""
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
7
|
+
|
|
8
|
+
from toybox_api.client import ToyBoxClient
|
|
9
|
+
|
|
10
|
+
async def main():
|
|
11
|
+
client = ToyBoxClient()
|
|
12
|
+
await client.connect()
|
|
13
|
+
print("Connected:", client._connected)
|
|
14
|
+
await client.close()
|
|
15
|
+
|
|
16
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Dump raw DDP collection data for debugging."""
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
9
|
+
|
|
10
|
+
from toybox_api.client import ToyBoxClient
|
|
11
|
+
|
|
12
|
+
async def main():
|
|
13
|
+
# Read creds from file
|
|
14
|
+
with open("/tmp/toybox_creds") as f:
|
|
15
|
+
lines = f.read().strip().split("\n")
|
|
16
|
+
email, password = lines[0], lines[1]
|
|
17
|
+
|
|
18
|
+
client = ToyBoxClient()
|
|
19
|
+
await client.connect()
|
|
20
|
+
await client.authenticate(email, password)
|
|
21
|
+
await client.setup()
|
|
22
|
+
|
|
23
|
+
# Dump ALL collections raw
|
|
24
|
+
for coll_name, docs in client._collections.items():
|
|
25
|
+
print(f"\n{'='*60}")
|
|
26
|
+
print(f"Collection: {coll_name} ({len(docs)} docs)")
|
|
27
|
+
print(f"{'='*60}")
|
|
28
|
+
for doc_id, doc in docs.items():
|
|
29
|
+
print(json.dumps(doc, indent=2, default=str))
|
|
30
|
+
|
|
31
|
+
# Also try the getPrintRequestsByIds method
|
|
32
|
+
users = client._collections.get("users", {})
|
|
33
|
+
for uid, udata in users.items():
|
|
34
|
+
profile = udata.get("profile", {})
|
|
35
|
+
last_print_id = profile.get("last_completed_print")
|
|
36
|
+
if last_print_id:
|
|
37
|
+
print(f"\n{'='*60}")
|
|
38
|
+
print(f"Fetching print request: {last_print_id}")
|
|
39
|
+
print(f"{'='*60}")
|
|
40
|
+
result = await client._call_method(
|
|
41
|
+
"getPrintRequestsByIds",
|
|
42
|
+
[{"requestIds": [last_print_id]}],
|
|
43
|
+
)
|
|
44
|
+
print(json.dumps(result, indent=2, default=str))
|
|
45
|
+
|
|
46
|
+
await client.close()
|
|
47
|
+
|
|
48
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Live test of the ToyBox DDP client.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
TOYBOX_EMAIL=stgarrity TOYBOX_PASSWORD=xxx python3 tests/test_live.py
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=logging.DEBUG,
|
|
16
|
+
stream=sys.stderr,
|
|
17
|
+
format="%(levelname)s %(name)s: %(message)s",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from toybox_api.client import ToyBoxClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def test():
|
|
24
|
+
email = os.environ.get("TOYBOX_EMAIL")
|
|
25
|
+
password = os.environ.get("TOYBOX_PASSWORD")
|
|
26
|
+
|
|
27
|
+
# Also try reading from .env file or creds file
|
|
28
|
+
if not email or not password:
|
|
29
|
+
for creds_path in [
|
|
30
|
+
os.path.join(os.path.dirname(__file__), "..", ".env"),
|
|
31
|
+
"/tmp/toybox_creds",
|
|
32
|
+
]:
|
|
33
|
+
if os.path.exists(creds_path):
|
|
34
|
+
with open(creds_path) as f:
|
|
35
|
+
content = f.read().strip()
|
|
36
|
+
if "=" in content:
|
|
37
|
+
for line in content.split("\n"):
|
|
38
|
+
line = line.strip()
|
|
39
|
+
if "=" in line and not line.startswith("#"):
|
|
40
|
+
k, v = line.split("=", 1)
|
|
41
|
+
os.environ[k] = v
|
|
42
|
+
else:
|
|
43
|
+
lines = content.split("\n")
|
|
44
|
+
if len(lines) >= 2:
|
|
45
|
+
os.environ["TOYBOX_EMAIL"] = lines[0].strip()
|
|
46
|
+
os.environ["TOYBOX_PASSWORD"] = lines[1].strip()
|
|
47
|
+
email = os.environ.get("TOYBOX_EMAIL")
|
|
48
|
+
password = os.environ.get("TOYBOX_PASSWORD")
|
|
49
|
+
if email and password:
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
if not email or not password:
|
|
53
|
+
print("Set TOYBOX_EMAIL and TOYBOX_PASSWORD env vars or create .env file")
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
client = ToyBoxClient()
|
|
57
|
+
try:
|
|
58
|
+
print("Connecting to DDP...")
|
|
59
|
+
await client.connect()
|
|
60
|
+
print("Connected ✓")
|
|
61
|
+
|
|
62
|
+
print("Authenticating...")
|
|
63
|
+
try:
|
|
64
|
+
await client.authenticate(email, password)
|
|
65
|
+
except Exception:
|
|
66
|
+
# Retry with username-style login
|
|
67
|
+
msg_id = client._next_id()
|
|
68
|
+
future = asyncio.get_event_loop().create_future()
|
|
69
|
+
client._pending[msg_id] = future
|
|
70
|
+
await client._send(
|
|
71
|
+
{
|
|
72
|
+
"msg": "method",
|
|
73
|
+
"method": "login",
|
|
74
|
+
"id": msg_id,
|
|
75
|
+
"params": [
|
|
76
|
+
{"user": {"username": email}, "password": password}
|
|
77
|
+
],
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
result = await asyncio.wait_for(future, timeout=15)
|
|
81
|
+
client._login_token = result.get("token")
|
|
82
|
+
client._user_id = result.get("id")
|
|
83
|
+
|
|
84
|
+
print(f"Authenticated ✓ (user_id={client._user_id})")
|
|
85
|
+
|
|
86
|
+
print("Running setup (discover printers, subscribe)...")
|
|
87
|
+
await client.setup()
|
|
88
|
+
print(f"Printer IDs: {client.printer_ids}")
|
|
89
|
+
print(f"Collections: {list(client._collections.keys())}")
|
|
90
|
+
for coll_name, docs in client._collections.items():
|
|
91
|
+
print(f" {coll_name}: {len(docs)} docs")
|
|
92
|
+
|
|
93
|
+
data = await client.get_all_data()
|
|
94
|
+
print(f"\n=== Printer ===")
|
|
95
|
+
print(f" Name: {data.printer.display_name}")
|
|
96
|
+
print(f" Model: {data.printer.model}")
|
|
97
|
+
print(f" Online: {data.printer.is_online}")
|
|
98
|
+
print(f" State: {data.print_state}")
|
|
99
|
+
print(f" Hardware ID: {data.printer.hardware_id}")
|
|
100
|
+
print(f" Firmware: {data.printer.firmware_version}")
|
|
101
|
+
print(f" Last Completed Print ID: {data.printer.last_completed_print}")
|
|
102
|
+
|
|
103
|
+
if data.current_request:
|
|
104
|
+
r = data.current_request
|
|
105
|
+
print(f"\n=== Current Print ===")
|
|
106
|
+
print(f" Name: {r.print_name}")
|
|
107
|
+
print(f" State: {r.state}")
|
|
108
|
+
print(f" Remaining: {r.remaining_seconds}s")
|
|
109
|
+
print(f" Progress: {r.progress_percent}%")
|
|
110
|
+
else:
|
|
111
|
+
print("\n No active print")
|
|
112
|
+
|
|
113
|
+
if data.last_completed_request:
|
|
114
|
+
r = data.last_completed_request
|
|
115
|
+
print(f"\n=== Last Completed ===")
|
|
116
|
+
print(f" Name: {r.print_name}")
|
|
117
|
+
print(f" State: {r.state} (end_reason={r.end_reason})")
|
|
118
|
+
if r.print_completion_time:
|
|
119
|
+
print(f" Completed at: {r.print_completion_time}")
|
|
120
|
+
else:
|
|
121
|
+
print("\n No last completed in subscriptions")
|
|
122
|
+
if data.printer.last_completed_print:
|
|
123
|
+
print(
|
|
124
|
+
f" Fetching via method call for ID={data.printer.last_completed_print}..."
|
|
125
|
+
)
|
|
126
|
+
details = await client.get_print_request_details(
|
|
127
|
+
[data.printer.last_completed_print]
|
|
128
|
+
)
|
|
129
|
+
for d in details:
|
|
130
|
+
apm = d.get("active_print_model", {})
|
|
131
|
+
print(f" Name: {apm.get('name')}")
|
|
132
|
+
print(
|
|
133
|
+
f" State: {d.get('state')} end_reason={d.get('end_reason')}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
print(f"\n=== Raw printerStates ===")
|
|
137
|
+
for doc_id, doc in client._collections.get("printerStates", {}).items():
|
|
138
|
+
print(json.dumps(doc, indent=2, default=str)[:800])
|
|
139
|
+
|
|
140
|
+
print(f"\n=== Raw toyPrints ===")
|
|
141
|
+
for doc_id, doc in client._collections.get("toyPrints", {}).items():
|
|
142
|
+
apm = doc.get("active_print_model", {})
|
|
143
|
+
name = apm.get("name") if isinstance(apm, dict) else None
|
|
144
|
+
print(
|
|
145
|
+
f" {doc_id}: state={doc.get('state')} "
|
|
146
|
+
f"active={doc.get('is_active')} name={name}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Also dump raw users collection to see printer ID structure
|
|
150
|
+
print(f"\n=== Raw users (printer fields only) ===")
|
|
151
|
+
for doc_id, doc in client._collections.get("users", {}).items():
|
|
152
|
+
filtered = {
|
|
153
|
+
k: v
|
|
154
|
+
for k, v in doc.items()
|
|
155
|
+
if k in ("_id", "printers", "profile", "username", "emails")
|
|
156
|
+
}
|
|
157
|
+
print(json.dumps(filtered, indent=2, default=str)[:600])
|
|
158
|
+
|
|
159
|
+
except Exception:
|
|
160
|
+
import traceback
|
|
161
|
+
|
|
162
|
+
traceback.print_exc()
|
|
163
|
+
finally:
|
|
164
|
+
await client.close()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
asyncio.run(test())
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: toybox-3d-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python API client for ToyBox 3D printers (make.toys) via Meteor DDP
|
|
5
|
+
Author: stgarrity
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/stgarrity/toybox-api
|
|
8
|
+
Project-URL: Repository, https://github.com/stgarrity/toybox-api
|
|
9
|
+
Project-URL: Issues, https://github.com/stgarrity/toybox-api/issues
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# toybox-api — Python Client for ToyBox 3D Printers
|
|
20
|
+
|
|
21
|
+
Async Python client for [ToyBox 3D printers](https://www.toybox.com/) via the [make.toys](https://www.make.toys) Meteor DDP protocol.
|
|
22
|
+
|
|
23
|
+
Used by the [Home Assistant integration](https://github.com/stgarrity/homeassistant-toybox).
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- Meteor DDP WebSocket client (no REST — make.toys is DDP-only)
|
|
28
|
+
- Authentication via Meteor accounts
|
|
29
|
+
- Real-time printer state via DDP subscriptions (`printerStates` collection)
|
|
30
|
+
- Print job tracking via `toyPrints` collection
|
|
31
|
+
- Time remaining / elapsed / progress calculations matching make.toys web app logic
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install toybox-api
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import asyncio
|
|
43
|
+
from toybox_api import ToyBoxClient
|
|
44
|
+
|
|
45
|
+
async def main():
|
|
46
|
+
async with ToyBoxClient() as client:
|
|
47
|
+
await client.connect()
|
|
48
|
+
await client.authenticate("email@example.com", "password")
|
|
49
|
+
await client.subscribe_to_printer_data(["printer_id"])
|
|
50
|
+
|
|
51
|
+
data = await client.get_all_data()
|
|
52
|
+
print(f"Printer: {data.printer.display_name}")
|
|
53
|
+
print(f"Online: {data.printer.is_online}")
|
|
54
|
+
print(f"State: {data.print_state}")
|
|
55
|
+
|
|
56
|
+
if data.current_request:
|
|
57
|
+
print(f"Printing: {data.current_request.print_name}")
|
|
58
|
+
print(f"Remaining: {data.current_request.remaining_seconds}s")
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Data Model
|
|
64
|
+
|
|
65
|
+
| Class | Source | Description |
|
|
66
|
+
|-------|--------|-------------|
|
|
67
|
+
| `PrinterStatus` | `printerStates` collection | Online state, model, hardware ID, firmware |
|
|
68
|
+
| `PrintRequest` | `toyPrints` collection | Active/completed prints with timing data |
|
|
69
|
+
| `ToyBoxData` | Coordinator container | Combines printer + current/last print request |
|
|
70
|
+
|
|
71
|
+
## Status
|
|
72
|
+
|
|
73
|
+
🟡 **Beta** — Implements real Meteor DDP protocol. Needs live testing with active prints.
|
|
74
|
+
|
|
75
|
+
## Project Structure
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
ha-toybox/
|
|
79
|
+
├── toybox_api/
|
|
80
|
+
│ ├── __init__.py
|
|
81
|
+
│ ├── client.py # DDP WebSocket client
|
|
82
|
+
│ ├── models.py # PrinterStatus, PrintRequest, ToyBoxData
|
|
83
|
+
│ ├── exceptions.py # ToyBoxError hierarchy
|
|
84
|
+
│ └── const.py # DDP URLs, subscription/method names
|
|
85
|
+
├── pyproject.toml
|
|
86
|
+
├── LICENSE
|
|
87
|
+
└── README.md
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tests/test_connection.py
|
|
5
|
+
tests/test_dump.py
|
|
6
|
+
tests/test_live.py
|
|
7
|
+
toybox_3d_api.egg-info/PKG-INFO
|
|
8
|
+
toybox_3d_api.egg-info/SOURCES.txt
|
|
9
|
+
toybox_3d_api.egg-info/dependency_links.txt
|
|
10
|
+
toybox_3d_api.egg-info/requires.txt
|
|
11
|
+
toybox_3d_api.egg-info/top_level.txt
|
|
12
|
+
toybox_api/__init__.py
|
|
13
|
+
toybox_api/client.py
|
|
14
|
+
toybox_api/const.py
|
|
15
|
+
toybox_api/exceptions.py
|
|
16
|
+
toybox_api/models.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
toybox_api
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""ToyBox API client - Python client for make.toys."""
|
|
2
|
+
|
|
3
|
+
from toybox_api.client import ToyBoxClient
|
|
4
|
+
from toybox_api.models import (
|
|
5
|
+
PrinterStatus,
|
|
6
|
+
PrintRequest,
|
|
7
|
+
ActivePrintModel,
|
|
8
|
+
PrintState,
|
|
9
|
+
PrintRequestState,
|
|
10
|
+
ToyBoxData,
|
|
11
|
+
)
|
|
12
|
+
from toybox_api.exceptions import (
|
|
13
|
+
ToyBoxError,
|
|
14
|
+
AuthenticationError,
|
|
15
|
+
ConnectionError,
|
|
16
|
+
APIError,
|
|
17
|
+
SessionExpiredError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ToyBoxClient",
|
|
22
|
+
"PrinterStatus",
|
|
23
|
+
"PrintRequest",
|
|
24
|
+
"ActivePrintModel",
|
|
25
|
+
"PrintState",
|
|
26
|
+
"PrintRequestState",
|
|
27
|
+
"ToyBoxData",
|
|
28
|
+
"ToyBoxError",
|
|
29
|
+
"AuthenticationError",
|
|
30
|
+
"ConnectionError",
|
|
31
|
+
"APIError",
|
|
32
|
+
"SessionExpiredError",
|
|
33
|
+
]
|