anycubic-cloud-api 0.1.0__py3-none-any.whl
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.
- anycubic_cloud_api/__init__.py +62 -0
- anycubic_cloud_api/anycubic_api.py +7 -0
- anycubic_cloud_api/api/base.py +400 -0
- anycubic_cloud_api/api/functions.py +2055 -0
- anycubic_cloud_api/api/mqtt.py +449 -0
- anycubic_cloud_api/const/api_endpoints.py +106 -0
- anycubic_cloud_api/const/const.py +47 -0
- anycubic_cloud_api/const/enums.py +97 -0
- anycubic_cloud_api/const/mqtt.py +13 -0
- anycubic_cloud_api/data_models/consumable.py +116 -0
- anycubic_cloud_api/data_models/files.py +429 -0
- anycubic_cloud_api/data_models/gcode_file.py +107 -0
- anycubic_cloud_api/data_models/orders.py +369 -0
- anycubic_cloud_api/data_models/print_response.py +71 -0
- anycubic_cloud_api/data_models/print_speed_mode.py +61 -0
- anycubic_cloud_api/data_models/printer.py +2753 -0
- anycubic_cloud_api/data_models/printer_properties.py +1107 -0
- anycubic_cloud_api/data_models/printing_settings.py +130 -0
- anycubic_cloud_api/data_models/project.py +1234 -0
- anycubic_cloud_api/exceptions/error_strings.py +340 -0
- anycubic_cloud_api/exceptions/exceptions.py +91 -0
- anycubic_cloud_api/helpers/helpers.py +240 -0
- anycubic_cloud_api/models/auth.py +396 -0
- anycubic_cloud_api/models/cloud_upload.py +249 -0
- anycubic_cloud_api/models/http.py +29 -0
- anycubic_cloud_api/resources/anycubic_mqqt_tls_ca.crt +24 -0
- anycubic_cloud_api/resources/anycubic_mqqt_tls_client.crt +24 -0
- anycubic_cloud_api/resources/anycubic_mqqt_tls_client.key +28 -0
- anycubic_cloud_api-0.1.0.dist-info/METADATA +79 -0
- anycubic_cloud_api-0.1.0.dist-info/RECORD +33 -0
- anycubic_cloud_api-0.1.0.dist-info/WHEEL +5 -0
- anycubic_cloud_api-0.1.0.dist-info/licenses/LICENSE +674 -0
- anycubic_cloud_api-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import ssl
|
|
6
|
+
import traceback
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from os import path
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from paho.mqtt import client as mqtt_client
|
|
12
|
+
|
|
13
|
+
# paho-mqtt 2.x requires CallbackAPIVersion as the first argument to Client().
|
|
14
|
+
# We pin VERSION1 so the existing callback signatures (on_connect with rc,
|
|
15
|
+
# on_disconnect with rc, on_message with msg, on_subscribe with mid/granted_qos)
|
|
16
|
+
# keep working without rewriting every callback in the file.
|
|
17
|
+
# Fall back to a sentinel for paho-mqtt 1.x where this enum doesn't exist.
|
|
18
|
+
try:
|
|
19
|
+
from paho.mqtt.enums import CallbackAPIVersion as _CallbackAPIVersion
|
|
20
|
+
_MQTT_CALLBACK_API_VERSION = _CallbackAPIVersion.VERSION1
|
|
21
|
+
except ImportError: # paho-mqtt 1.x
|
|
22
|
+
_MQTT_CALLBACK_API_VERSION = None
|
|
23
|
+
|
|
24
|
+
from ..const.mqtt import (
|
|
25
|
+
MQTT_HOST,
|
|
26
|
+
MQTT_PORT,
|
|
27
|
+
MQTT_ROOT_TOPIC_PLUS,
|
|
28
|
+
MQTT_ROOT_TOPIC_PRINTER,
|
|
29
|
+
MQTT_ROOT_TOPIC_PUBLISH_PRINTER,
|
|
30
|
+
MQTT_ROOT_TOPIC_SERVER,
|
|
31
|
+
MQTT_TIMEOUT,
|
|
32
|
+
)
|
|
33
|
+
from ..data_models.consumable import AnycubicConsumableData
|
|
34
|
+
from ..exceptions.error_strings import ErrorsMQTTClient
|
|
35
|
+
from ..exceptions.exceptions import (
|
|
36
|
+
AnycubicMQTTClientError,
|
|
37
|
+
AnycubicMQTTUnhandledData,
|
|
38
|
+
AnycubicMQTTUnknownUpdate,
|
|
39
|
+
)
|
|
40
|
+
from ..helpers.helpers import (
|
|
41
|
+
get_mqtt_ssl_path_ca,
|
|
42
|
+
get_mqtt_ssl_path_cert,
|
|
43
|
+
get_mqtt_ssl_path_key,
|
|
44
|
+
get_part_from_mqtt_topic,
|
|
45
|
+
get_ssl_cert_directory,
|
|
46
|
+
redact_part_from_mqtt_topic,
|
|
47
|
+
)
|
|
48
|
+
from .functions import AnycubicAPIFunctions
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from ..data_models.printer import AnycubicPrinter
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AnycubicMQTTAPI(AnycubicAPIFunctions):
|
|
55
|
+
__slots__ = (
|
|
56
|
+
"_mqtt_client",
|
|
57
|
+
"_mqtt_subscribed_printers",
|
|
58
|
+
"_mqtt_log_all_messages",
|
|
59
|
+
"_mqtt_connected",
|
|
60
|
+
"_mqtt_disconnected",
|
|
61
|
+
"_mqtt_callback_printer_update",
|
|
62
|
+
"_mqtt_callback_printer_busy",
|
|
63
|
+
"_mqtt_callback_subscribed",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
*args: Any,
|
|
69
|
+
mqtt_callback_printer_update: Callable[[], None] | None = None,
|
|
70
|
+
mqtt_callback_printer_busy: Callable[[], None] | None = None,
|
|
71
|
+
mqtt_callback_subscribed: Callable[[], None] | None = None,
|
|
72
|
+
**kwargs: Any,
|
|
73
|
+
) -> None:
|
|
74
|
+
self._mqtt_client: mqtt_client.Client | None = None
|
|
75
|
+
self._mqtt_subscribed_printers: dict[str, AnycubicPrinter] = dict()
|
|
76
|
+
self._mqtt_log_all_messages: bool = False
|
|
77
|
+
self._mqtt_connected: asyncio.Event | None = None
|
|
78
|
+
self._mqtt_disconnected: asyncio.Event | None = None
|
|
79
|
+
self._mqtt_callback_printer_update: Callable[[], None] | None = mqtt_callback_printer_update
|
|
80
|
+
self._mqtt_callback_printer_busy: Callable[[], None] | None = mqtt_callback_printer_busy
|
|
81
|
+
self._mqtt_callback_subscribed: Callable[[], None] | None = mqtt_callback_subscribed
|
|
82
|
+
super().__init__(*args, **kwargs)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def mqtt_is_started(self) -> bool:
|
|
86
|
+
return self._mqtt_client is not None
|
|
87
|
+
|
|
88
|
+
def set_mqtt_log_all_messages(self, val: bool) -> None:
|
|
89
|
+
self._mqtt_log_all_messages = bool(val)
|
|
90
|
+
|
|
91
|
+
async def mqtt_wait_for_connect(self) -> bool:
|
|
92
|
+
if self._mqtt_connected is None:
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
async with asyncio.timeout(10):
|
|
97
|
+
await self._mqtt_connected.wait()
|
|
98
|
+
self._mqtt_connected = None
|
|
99
|
+
await asyncio.sleep(2)
|
|
100
|
+
return True
|
|
101
|
+
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
async def mqtt_wait_for_disconnect(self) -> bool:
|
|
105
|
+
if self._mqtt_disconnected is None:
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
async with asyncio.timeout(10):
|
|
110
|
+
await self._mqtt_disconnected.wait()
|
|
111
|
+
self._mqtt_disconnected = None
|
|
112
|
+
return True
|
|
113
|
+
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def _build_mqtt_printer_subscription(self, printer: AnycubicPrinter) -> list[str]:
|
|
117
|
+
# topic_slicer = f"{MQTT_ROOT_TOPIC_SLICER}{printer.machine_type}/{printer.key}/#"
|
|
118
|
+
topic_printer = f"{MQTT_ROOT_TOPIC_PRINTER}{printer.machine_type}/{printer.key}/#"
|
|
119
|
+
topic_plus = f"{MQTT_ROOT_TOPIC_PLUS}{printer.machine_type}/{printer.key}/#"
|
|
120
|
+
return list([
|
|
121
|
+
# topic_slicer,
|
|
122
|
+
topic_printer,
|
|
123
|
+
topic_plus
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
def _build_mqtt_printer_publish_topic(self, printer: AnycubicPrinter, endpoint: str) -> str:
|
|
127
|
+
return f"{MQTT_ROOT_TOPIC_PUBLISH_PRINTER}{printer.machine_type}/{printer.key}/{endpoint}"
|
|
128
|
+
|
|
129
|
+
def _build_mqtt_user_subscription(self) -> list[str]:
|
|
130
|
+
user_id, user_id_md5 = self.anycubic_auth.get_user_id_md5_tuple()
|
|
131
|
+
root = f"{MQTT_ROOT_TOPIC_SERVER}{user_id}/{user_id_md5}"
|
|
132
|
+
topic_slice_report = f"{root}/slice/report"
|
|
133
|
+
topic_fdm_slice_report = f"{root}/fdmslice/report"
|
|
134
|
+
|
|
135
|
+
return list([topic_slice_report, topic_fdm_slice_report])
|
|
136
|
+
|
|
137
|
+
def _mqtt_topic_is_user_topic(self, topic: str) -> bool:
|
|
138
|
+
return get_part_from_mqtt_topic(topic, 3) == 'server'
|
|
139
|
+
|
|
140
|
+
def _mqtt_message_router(self, message: mqtt_client.MQTTMessage) -> None:
|
|
141
|
+
try:
|
|
142
|
+
topic = str(message.topic)
|
|
143
|
+
payload = json.loads(message.payload.decode('utf-8'))
|
|
144
|
+
except Exception as e:
|
|
145
|
+
self._log_to_error(
|
|
146
|
+
f"Anycubic MQTT Message decode error: {e}\n"
|
|
147
|
+
f" on MQTT topic: {message.topic}\n"
|
|
148
|
+
f" {message.payload!r}"
|
|
149
|
+
)
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
if self._mqtt_topic_is_user_topic(topic):
|
|
153
|
+
self._log_to_debug(
|
|
154
|
+
f"Anycubic MQTT USER Msg Received on `{topic}`:\n"
|
|
155
|
+
f" {payload}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
else:
|
|
159
|
+
printer_key = get_part_from_mqtt_topic(topic, 6)
|
|
160
|
+
|
|
161
|
+
if get_part_from_mqtt_topic(topic, 7) == 'response' and len(payload.keys()) == 1:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
if printer_key not in self._mqtt_subscribed_printers:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
printer = self._mqtt_subscribed_printers[printer_key]
|
|
168
|
+
|
|
169
|
+
printer_was_available = printer.is_available
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
printer.process_mqtt_update(topic, AnycubicConsumableData(payload))
|
|
173
|
+
|
|
174
|
+
except AnycubicMQTTUnhandledData as e:
|
|
175
|
+
redacted_topic = redact_part_from_mqtt_topic(topic, 6)
|
|
176
|
+
self._log_to_warn(
|
|
177
|
+
f"Anycubic MQTT Message unhandled data in: {e}\n"
|
|
178
|
+
f" on MQTT topic: {redacted_topic}\n"
|
|
179
|
+
f" with type: {e.unhandled_mqtt_type}, "
|
|
180
|
+
f"action: {e.unhandled_mqtt_action}, "
|
|
181
|
+
f"state: {e.unhandled_mqtt_state}\n"
|
|
182
|
+
f" unhandled data: {e.unhandled_mqtt_data}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
except AnycubicMQTTUnknownUpdate as e:
|
|
186
|
+
# A message type this version doesn't parse yet. Nothing is
|
|
187
|
+
# broken and there is nothing the user can act on, so log a
|
|
188
|
+
# single debug line rather than an ERROR with a traceback.
|
|
189
|
+
redacted_topic = redact_part_from_mqtt_topic(topic, 6)
|
|
190
|
+
self._log_to_debug(
|
|
191
|
+
f"Anycubic MQTT Message not understood: {e}\n"
|
|
192
|
+
f" on MQTT topic: {redacted_topic}\n"
|
|
193
|
+
f" {payload}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
tb = traceback.format_exc()
|
|
198
|
+
redacted_topic = redact_part_from_mqtt_topic(topic, 6)
|
|
199
|
+
error_type = type(e)
|
|
200
|
+
self._log_to_error(
|
|
201
|
+
f"Anycubic MQTT Message error: {error_type}: {e}\n"
|
|
202
|
+
f" on MQTT topic: {redacted_topic}\n"
|
|
203
|
+
f" {payload}\n"
|
|
204
|
+
f"{tb}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
if self._mqtt_callback_printer_update:
|
|
209
|
+
self._mqtt_callback_printer_update()
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
self._mqtt_callback_printer_busy and
|
|
213
|
+
printer_was_available and printer.is_busy
|
|
214
|
+
):
|
|
215
|
+
self._mqtt_callback_printer_busy()
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
tb = traceback.format_exc()
|
|
219
|
+
self._log_to_error(
|
|
220
|
+
f"Anycubic MQTT Callback error: {e}\n"
|
|
221
|
+
f"{tb}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if self._mqtt_log_all_messages:
|
|
225
|
+
# Redact the printer key like the warning/error paths do --
|
|
226
|
+
# debug logs get pasted into issue reports.
|
|
227
|
+
self._log_to_debug(
|
|
228
|
+
f"Anycubic MQTT Message processed on topic: "
|
|
229
|
+
f"{redact_part_from_mqtt_topic(topic, 6)}\n"
|
|
230
|
+
f" {payload}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def _mqtt_publish_on_topic(
|
|
234
|
+
self,
|
|
235
|
+
topic: str,
|
|
236
|
+
payload: dict[str, Any] | str,
|
|
237
|
+
) -> None:
|
|
238
|
+
if self._mqtt_client is None:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
mqtt_payload = json.dumps(payload) if isinstance(payload, dict) else payload
|
|
242
|
+
|
|
243
|
+
self._mqtt_client.publish(topic, payload=mqtt_payload)
|
|
244
|
+
|
|
245
|
+
def _mqtt_publish_to_printer(
|
|
246
|
+
self,
|
|
247
|
+
printer: AnycubicPrinter,
|
|
248
|
+
endpoint: str,
|
|
249
|
+
payload: dict[str, Any] | str,
|
|
250
|
+
) -> None:
|
|
251
|
+
mqtt_topic = self._build_mqtt_printer_publish_topic(printer, endpoint)
|
|
252
|
+
self._mqtt_publish_on_topic(mqtt_topic, payload=payload)
|
|
253
|
+
|
|
254
|
+
def _mqtt_build_ssl_context(self) -> ssl.SSLContext:
|
|
255
|
+
ssl_root = get_ssl_cert_directory()
|
|
256
|
+
|
|
257
|
+
crt_path = get_mqtt_ssl_path_cert(ssl_root)
|
|
258
|
+
|
|
259
|
+
if not path.exists(crt_path):
|
|
260
|
+
self._log_to_error(f"Anycubic MQTT unable to start, no certificate found in root: {ssl_root}.")
|
|
261
|
+
raise AnycubicMQTTClientError(ErrorsMQTTClient.cert_missing)
|
|
262
|
+
|
|
263
|
+
# Anycubic's broker is a fixed cloud endpoint, so verify it properly.
|
|
264
|
+
# PROTOCOL_TLS_CLIENT enables certificate verification and hostname
|
|
265
|
+
# checking by default; both are set explicitly below to make that clear.
|
|
266
|
+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
267
|
+
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
268
|
+
|
|
269
|
+
# Anycubic's client certificate is SHA-1 signed, which OpenSSL 3.x will
|
|
270
|
+
# not load at the default security level. Narrowed from upstream's
|
|
271
|
+
# "ALL:" so the normal cipher list still applies -- this relaxes the
|
|
272
|
+
# certificate signature check only.
|
|
273
|
+
ssl_context.set_ciphers('DEFAULT:@SECLEVEL=0')
|
|
274
|
+
|
|
275
|
+
# Anycubic's root CA omits the keyUsage extension. Python 3.13+ enables
|
|
276
|
+
# VERIFY_X509_STRICT by default, which rejects such a CA as a trust
|
|
277
|
+
# anchor outright. Clearing that single flag keeps full chain and
|
|
278
|
+
# hostname verification intact. No-op on older Python versions.
|
|
279
|
+
ssl_context.verify_flags &= ~ssl.VERIFY_X509_STRICT
|
|
280
|
+
|
|
281
|
+
ssl_context.check_hostname = True
|
|
282
|
+
ssl_context.verify_mode = ssl.CERT_REQUIRED
|
|
283
|
+
|
|
284
|
+
# Pin Anycubic's own root CA -- their broker certificate is issued by a
|
|
285
|
+
# private CA and is not present in any public trust store.
|
|
286
|
+
ssl_context.load_verify_locations(get_mqtt_ssl_path_ca(ssl_root))
|
|
287
|
+
ssl_context.load_cert_chain(
|
|
288
|
+
crt_path,
|
|
289
|
+
get_mqtt_ssl_path_key(ssl_root),
|
|
290
|
+
None,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return ssl_context
|
|
294
|
+
|
|
295
|
+
def _mqtt_on_subscribe(
|
|
296
|
+
self,
|
|
297
|
+
client: mqtt_client.Client,
|
|
298
|
+
userdata: Any,
|
|
299
|
+
mid: int,
|
|
300
|
+
granted_qos: tuple[int],
|
|
301
|
+
) -> None:
|
|
302
|
+
if self._mqtt_connected is not None:
|
|
303
|
+
self._mqtt_connected.set()
|
|
304
|
+
|
|
305
|
+
def _mqtt_on_message(
|
|
306
|
+
self,
|
|
307
|
+
client: mqtt_client.Client,
|
|
308
|
+
userdata: Any,
|
|
309
|
+
message: mqtt_client.MQTTMessage,
|
|
310
|
+
) -> None:
|
|
311
|
+
try:
|
|
312
|
+
self._mqtt_message_router(message)
|
|
313
|
+
except Exception as e:
|
|
314
|
+
tb = traceback.format_exc()
|
|
315
|
+
self._log_to_error(f"Anycubic MQTT internal error: {e}\n{tb}")
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
def _mqtt_on_disconnect(
|
|
319
|
+
self,
|
|
320
|
+
client: mqtt_client.Client,
|
|
321
|
+
userdata: Any,
|
|
322
|
+
rc: int,
|
|
323
|
+
) -> None:
|
|
324
|
+
if rc == 0:
|
|
325
|
+
self._mqtt_client = None
|
|
326
|
+
self._log_to_debug("Anycubic MQTT Disconnected.")
|
|
327
|
+
if self._mqtt_disconnected is not None:
|
|
328
|
+
self._mqtt_disconnected.set()
|
|
329
|
+
else:
|
|
330
|
+
self._set_mqtt_username_password()
|
|
331
|
+
self._log_to_debug("Anycubic MQTT unintentionally disconnected, will reconnect.")
|
|
332
|
+
|
|
333
|
+
def _mqtt_on_connect(
|
|
334
|
+
self,
|
|
335
|
+
client: mqtt_client.Client,
|
|
336
|
+
userdata: Any,
|
|
337
|
+
flags: dict[str, Any],
|
|
338
|
+
rc: int,
|
|
339
|
+
) -> None:
|
|
340
|
+
if rc == 0:
|
|
341
|
+
if self._mqtt_connected is None:
|
|
342
|
+
self._mqtt_connected = asyncio.Event()
|
|
343
|
+
|
|
344
|
+
self._log_to_debug("Anycubic MQTT Connected.")
|
|
345
|
+
|
|
346
|
+
if not self._mqtt_client:
|
|
347
|
+
raise AnycubicMQTTClientError(ErrorsMQTTClient.connect_client_missing)
|
|
348
|
+
|
|
349
|
+
for sub in self._build_mqtt_user_subscription():
|
|
350
|
+
self._log_to_debug(f"Anycubic MQTT Subscribing to USER {sub}.")
|
|
351
|
+
self._mqtt_client.subscribe(sub)
|
|
352
|
+
|
|
353
|
+
for printer_id, printer in self._mqtt_subscribed_printers.items():
|
|
354
|
+
self._mqtt_subscribe_printer_status(printer)
|
|
355
|
+
|
|
356
|
+
self._log_to_debug("Anycubic MQTT Subscribed.")
|
|
357
|
+
|
|
358
|
+
if (self._mqtt_callback_subscribed):
|
|
359
|
+
self._mqtt_callback_subscribed()
|
|
360
|
+
else:
|
|
361
|
+
self._log_to_warn(f"Anycubic MQTT Failed to connect, return code {rc}")
|
|
362
|
+
|
|
363
|
+
def _set_mqtt_username_password(self) -> None:
|
|
364
|
+
mqtt_username, mqtt_password = self.anycubic_auth.get_mqtt_login_info()
|
|
365
|
+
|
|
366
|
+
if self._mqtt_client is None:
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
self._mqtt_client.username_pw_set(
|
|
370
|
+
username=mqtt_username,
|
|
371
|
+
password=mqtt_password,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def connect_mqtt(self) -> None:
|
|
375
|
+
self._mqtt_connected = asyncio.Event()
|
|
376
|
+
self._mqtt_disconnected = asyncio.Event()
|
|
377
|
+
|
|
378
|
+
self._log_to_debug("Anycubic MQTT Connecting.")
|
|
379
|
+
|
|
380
|
+
# paho-mqtt 2.x: pass callback_api_version positionally; 1.x: omit.
|
|
381
|
+
if _MQTT_CALLBACK_API_VERSION is not None:
|
|
382
|
+
self._mqtt_client = mqtt_client.Client(
|
|
383
|
+
_MQTT_CALLBACK_API_VERSION,
|
|
384
|
+
client_id=self.anycubic_auth.get_mqtt_client_id(),
|
|
385
|
+
clean_session=True,
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
self._mqtt_client = mqtt_client.Client(
|
|
389
|
+
client_id=self.anycubic_auth.get_mqtt_client_id(),
|
|
390
|
+
clean_session=True,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
self._mqtt_client.on_connect = self._mqtt_on_connect
|
|
394
|
+
self._mqtt_client.on_disconnect = self._mqtt_on_disconnect
|
|
395
|
+
self._mqtt_client.on_message = self._mqtt_on_message
|
|
396
|
+
self._mqtt_client.on_subscribe = self._mqtt_on_subscribe
|
|
397
|
+
|
|
398
|
+
self._set_mqtt_username_password()
|
|
399
|
+
|
|
400
|
+
self._mqtt_client.tls_set_context(self._mqtt_build_ssl_context())
|
|
401
|
+
|
|
402
|
+
self._mqtt_client.reconnect_delay_set(5)
|
|
403
|
+
|
|
404
|
+
self._mqtt_client.connect(
|
|
405
|
+
host=MQTT_HOST,
|
|
406
|
+
port=MQTT_PORT,
|
|
407
|
+
keepalive=MQTT_TIMEOUT,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
self._mqtt_client.loop_forever()
|
|
411
|
+
self._mqtt_client = None
|
|
412
|
+
if self._mqtt_disconnected is not None:
|
|
413
|
+
self._mqtt_disconnected.set()
|
|
414
|
+
self._mqtt_connected = None
|
|
415
|
+
self._mqtt_disconnected = None
|
|
416
|
+
self._log_to_debug("Anycubic MQTT Client removed.")
|
|
417
|
+
|
|
418
|
+
def disconnect_mqtt(self) -> None:
|
|
419
|
+
self._log_to_debug("Anycubic MQTT Disconnecting.")
|
|
420
|
+
if self._mqtt_client is None:
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
self._mqtt_client.disconnect()
|
|
424
|
+
|
|
425
|
+
def _mqtt_subscribe_printer_status(self, printer: AnycubicPrinter) -> None:
|
|
426
|
+
if not self._mqtt_client:
|
|
427
|
+
raise AnycubicMQTTClientError(ErrorsMQTTClient.sub_printer_status_client_missing)
|
|
428
|
+
for sub in self._build_mqtt_printer_subscription(printer):
|
|
429
|
+
self._log_to_debug(f"Anycubic MQTT Subscribing to PRINTER {sub}.")
|
|
430
|
+
self._mqtt_client.subscribe(sub)
|
|
431
|
+
|
|
432
|
+
def mqtt_add_subscribed_printer(self, printer: AnycubicPrinter) -> None:
|
|
433
|
+
if not printer.key:
|
|
434
|
+
raise AnycubicMQTTClientError(ErrorsMQTTClient.sub_printer_key_missing)
|
|
435
|
+
|
|
436
|
+
if printer.key in self._mqtt_subscribed_printers:
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
self._mqtt_subscribed_printers[printer.key] = printer
|
|
440
|
+
|
|
441
|
+
def mqtt_unsubscribe_printer_status(self, printer: AnycubicPrinter) -> None:
|
|
442
|
+
|
|
443
|
+
if printer.key in self._mqtt_subscribed_printers:
|
|
444
|
+
if self._mqtt_client is not None:
|
|
445
|
+
for sub in self._build_mqtt_printer_subscription(printer):
|
|
446
|
+
self._log_to_debug(f"Anycubic MQTT Ubsubscribing to PRINTER {sub}.")
|
|
447
|
+
self._mqtt_client.unsubscribe(sub)
|
|
448
|
+
|
|
449
|
+
del self._mqtt_subscribed_printers[printer.key]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..models.http import HTTP_METHODS, AnycubicAPIEndpoint
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class API_ENDPOINT:
|
|
7
|
+
oauth_token_url = AnycubicAPIEndpoint(
|
|
8
|
+
HTTP_METHODS.GET,
|
|
9
|
+
'/v3/public/getoauthToken',
|
|
10
|
+
)
|
|
11
|
+
auth_sig_token = AnycubicAPIEndpoint(
|
|
12
|
+
HTTP_METHODS.POST,
|
|
13
|
+
'/v3/public/loginWithAccessToken',
|
|
14
|
+
)
|
|
15
|
+
user_store = AnycubicAPIEndpoint(
|
|
16
|
+
HTTP_METHODS.POST,
|
|
17
|
+
'/work/index/getUserStore',
|
|
18
|
+
)
|
|
19
|
+
lock_storage_space = AnycubicAPIEndpoint(
|
|
20
|
+
HTTP_METHODS.POST,
|
|
21
|
+
'/v2/cloud_storage/lockStorageSpace',
|
|
22
|
+
)
|
|
23
|
+
unlock_storage_space = AnycubicAPIEndpoint(
|
|
24
|
+
HTTP_METHODS.POST,
|
|
25
|
+
'/v2/cloud_storage/unlockStorageSpace',
|
|
26
|
+
)
|
|
27
|
+
new_file_upload = AnycubicAPIEndpoint(
|
|
28
|
+
HTTP_METHODS.POST,
|
|
29
|
+
'/v2/profile/newUploadFile',
|
|
30
|
+
)
|
|
31
|
+
delete_cloud_file = AnycubicAPIEndpoint(
|
|
32
|
+
HTTP_METHODS.POST,
|
|
33
|
+
'/work/index/delFiles',
|
|
34
|
+
)
|
|
35
|
+
user_files = AnycubicAPIEndpoint(
|
|
36
|
+
HTTP_METHODS.POST,
|
|
37
|
+
'/work/index/files',
|
|
38
|
+
)
|
|
39
|
+
user_info = AnycubicAPIEndpoint(
|
|
40
|
+
HTTP_METHODS.GET,
|
|
41
|
+
'/user/profile/userInfo',
|
|
42
|
+
)
|
|
43
|
+
printer_status = AnycubicAPIEndpoint(
|
|
44
|
+
HTTP_METHODS.GET,
|
|
45
|
+
'/v2/Printer/status',
|
|
46
|
+
)
|
|
47
|
+
printer_info = AnycubicAPIEndpoint(
|
|
48
|
+
HTTP_METHODS.GET,
|
|
49
|
+
'/v2/printer/info',
|
|
50
|
+
)
|
|
51
|
+
printer_tool = AnycubicAPIEndpoint(
|
|
52
|
+
HTTP_METHODS.GET,
|
|
53
|
+
'/v2/printer/tool',
|
|
54
|
+
)
|
|
55
|
+
printer_functions = AnycubicAPIEndpoint(
|
|
56
|
+
HTTP_METHODS.GET,
|
|
57
|
+
'/v2/printer/functions',
|
|
58
|
+
)
|
|
59
|
+
printer_all = AnycubicAPIEndpoint(
|
|
60
|
+
HTTP_METHODS.GET,
|
|
61
|
+
'/v2/printer/all',
|
|
62
|
+
)
|
|
63
|
+
printers_status = AnycubicAPIEndpoint(
|
|
64
|
+
HTTP_METHODS.GET,
|
|
65
|
+
'/work/printer/printersStatus',
|
|
66
|
+
)
|
|
67
|
+
printer_get_printers = AnycubicAPIEndpoint(
|
|
68
|
+
HTTP_METHODS.GET,
|
|
69
|
+
'/work/printer/getPrinters',
|
|
70
|
+
)
|
|
71
|
+
print_history = AnycubicAPIEndpoint(
|
|
72
|
+
HTTP_METHODS.GET,
|
|
73
|
+
'/v2/project/printHistory',
|
|
74
|
+
)
|
|
75
|
+
project_info = AnycubicAPIEndpoint(
|
|
76
|
+
HTTP_METHODS.GET,
|
|
77
|
+
'/v2/project/info',
|
|
78
|
+
)
|
|
79
|
+
project_monitor = AnycubicAPIEndpoint(
|
|
80
|
+
HTTP_METHODS.GET,
|
|
81
|
+
'/v2/project/monitor',
|
|
82
|
+
)
|
|
83
|
+
project_get_projects = AnycubicAPIEndpoint(
|
|
84
|
+
HTTP_METHODS.GET,
|
|
85
|
+
'/work/project/getProjects',
|
|
86
|
+
)
|
|
87
|
+
project_gcode_info_fdm = AnycubicAPIEndpoint(
|
|
88
|
+
HTTP_METHODS.GET,
|
|
89
|
+
'/work/gcode/infoFdm',
|
|
90
|
+
)
|
|
91
|
+
send_order = AnycubicAPIEndpoint(
|
|
92
|
+
HTTP_METHODS.POST,
|
|
93
|
+
'/work/operation/sendOrder',
|
|
94
|
+
)
|
|
95
|
+
printer_update_name = AnycubicAPIEndpoint(
|
|
96
|
+
HTTP_METHODS.POST,
|
|
97
|
+
'/work/printer/Info',
|
|
98
|
+
)
|
|
99
|
+
printer_firmware_update = AnycubicAPIEndpoint(
|
|
100
|
+
HTTP_METHODS.GET,
|
|
101
|
+
'/work/printer/update_version',
|
|
102
|
+
)
|
|
103
|
+
printer_multi_color_box_firmware_update = AnycubicAPIEndpoint(
|
|
104
|
+
HTTP_METHODS.POST,
|
|
105
|
+
'/v2/printer/update_multi_color_box_version'
|
|
106
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
BASE_DOMAIN = "cloud-universe.anycubic.com"
|
|
4
|
+
APP_REDIRECT_URI = "anycubic-i18n://cloud.anycubic.com:8088"
|
|
5
|
+
AUTH_DOMAIN = "uc.makeronline.com"
|
|
6
|
+
|
|
7
|
+
PUBLIC_API_ENDPOINT = "p/p/workbench/api"
|
|
8
|
+
|
|
9
|
+
PROJECT_IMAGE_URL_BASE = "https://workbentch.s3.us-east-2.amazonaws.com/"
|
|
10
|
+
|
|
11
|
+
REX_JS_FILE = re.compile(r'src="(/js/app\.[^.]+\.js)"')
|
|
12
|
+
# REX_CLIENT_ID = re.compile(r',clientId:"([^"]+)",')
|
|
13
|
+
REX_CLIENT_ID = re.compile(r'\'(?!getEl)([a-zA-Z0-9]{20})\'')
|
|
14
|
+
REX_APP_ID_BASIC = re.compile(r'appid["\']?:["\']([^"\']+)["\'],')
|
|
15
|
+
REX_APP_ID_OBF = re.compile(r'(?<!:)\'([a-zA-Z0-9]{32})\'')
|
|
16
|
+
REX_APP_VERSION = re.compile(r'version["\']?:["\']([^"\']+)["\'],')
|
|
17
|
+
REX_APP_SECRET_BASIC = re.compile(r'appSecret["\']?:["\']([^"\']+)["\'],')
|
|
18
|
+
REX_APP_SECRET_OBF = re.compile(r'(?<!:)\'([a-zA-Z0-9]{32})\'')
|
|
19
|
+
REX_GCODE_EXT = re.compile(r'\.gcode$')
|
|
20
|
+
|
|
21
|
+
DEFAULT_USER_AGENT = (
|
|
22
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
|
|
23
|
+
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 '
|
|
24
|
+
'Safari/537.36'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
AC_KNOWN_CID_WEB = '672efcd4ec11a66c8513'
|
|
28
|
+
AC_KNOWN_CID_APP = 'ca4c8416cced85a1dc02'
|
|
29
|
+
AC_KNOWN_AID = 'f9b3528877c94d5c9c5af32245db46ef'
|
|
30
|
+
AC_KNOWN_VID_WEB = '1.0.0'
|
|
31
|
+
AC_KNOWN_VID_APP = '1.4.8'
|
|
32
|
+
AC_KNOWN_VID_SLICER_NEXT = 'V3.0.0'
|
|
33
|
+
AC_KNOWN_SEC = '0cf75926606049a3937f56b0373b99fb'
|
|
34
|
+
|
|
35
|
+
MAX_PROJECT_LIST_RESULTS = 2000
|
|
36
|
+
MAX_PROJECT_IMAGE_SEARCH_COUNT = 200
|
|
37
|
+
|
|
38
|
+
MAX_API_FETCH_TIME_WARN = 20
|
|
39
|
+
|
|
40
|
+
WARN_INTERVAL_API_DURATION = 60 * 10
|
|
41
|
+
|
|
42
|
+
ACCESS_TOKEN_LOGIN_RETRIES = 2
|
|
43
|
+
ACCESS_TOKEN_LOGIN_RETRY_INTERVAL = 2
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AnycubicServerMessage:
|
|
47
|
+
FILE_NOT_FOUND = "No file found"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from enum import IntEnum, StrEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AnycubicFeedType(IntEnum):
|
|
5
|
+
Feed = 1
|
|
6
|
+
Retract = 2
|
|
7
|
+
Finish = 3
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AnycubicPrintStatus(IntEnum):
|
|
11
|
+
Printing = 1
|
|
12
|
+
Complete = 2
|
|
13
|
+
Cancelled = 3
|
|
14
|
+
Downloading = 4
|
|
15
|
+
Checking = 5
|
|
16
|
+
Preheating = 6
|
|
17
|
+
Slicing = 7
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AnycubicOrderID(IntEnum):
|
|
21
|
+
START_PRINT = 1
|
|
22
|
+
PAUSE_PRINT = 2
|
|
23
|
+
RESUME_PRINT = 3
|
|
24
|
+
STOP_PRINT = 4
|
|
25
|
+
PRINT_SETTINGS = 6
|
|
26
|
+
IGNORE = 11 # Not handled
|
|
27
|
+
DETECT = 12 # Not handled, appears un-used.
|
|
28
|
+
STOP_PRINT_FORCE = 44
|
|
29
|
+
LIST_UDISK_FILES = 101
|
|
30
|
+
DELETE_UDISK_FILE = 102
|
|
31
|
+
LIST_LOCAL_FILES = 103
|
|
32
|
+
DELETE_LOCAL_FILE = 104
|
|
33
|
+
MOVE_AXLE = 201 # Not handled
|
|
34
|
+
MOVE_AXLE_TO_COORDINATES = 202 # Not handled, appears un-used.
|
|
35
|
+
START_EXPOSURE = 301 # Not handled, appears un-used.
|
|
36
|
+
CANCEL_EXPOSURE = 302 # Not handled, appears un-used.
|
|
37
|
+
START_RESIDUAL = 501 # Not handled, appears un-used.
|
|
38
|
+
CANCEL_RESIDUAL = 502 # Not handled
|
|
39
|
+
SET_DEVICE_SELF_TEST = 601 # Not handled, appears un-used.
|
|
40
|
+
GET_DEVICE_SELF_TEST = 602 # Not handled
|
|
41
|
+
SET_AUTO_OPERATION = 701 # Not handled, appears un-used.
|
|
42
|
+
GET_AUTO_OPERATION = 702 # Not handled
|
|
43
|
+
RESET_RELEASE_FILM = 801 # Not handled
|
|
44
|
+
GET_RELEASE_FILM = 802 # Not handled
|
|
45
|
+
SET_PRINT_STATUS_FREE = 901 # Not handled, appears un-used.
|
|
46
|
+
CAMERA_OPEN = 1001
|
|
47
|
+
CAMERA_CLOSE = 1002 # Not handled, appears un-used.
|
|
48
|
+
MULTI_COLOR_BOX_GET_INFO = 1206
|
|
49
|
+
MULTI_COLOR_BOX_DRY = 1207
|
|
50
|
+
FEED_FILAMENT = 1208
|
|
51
|
+
FEED_FILAMENT_FINISH = 1209 # Not handled, appears un-used.
|
|
52
|
+
MULTI_COLOR_BOX_REFRESH_SLOT = 1210 # Not handled
|
|
53
|
+
MULTI_COLOR_BOX_SET_SLOT = 1211
|
|
54
|
+
MULTI_COLOR_BOX_AUTO_FEED = 1212
|
|
55
|
+
MOVE_AXLE_TURN_OFF = 1213 # Not handled
|
|
56
|
+
FILAMENT_CONTROL = 1215 # Not handled
|
|
57
|
+
FEED_RESIN = 1224 # Not handled
|
|
58
|
+
M7_AUTO_OPERATION = 1225 # Not handled
|
|
59
|
+
CYCLIC_CLEANING = 1226 # Not handled
|
|
60
|
+
SET_AUTO_FEED_INFO = 1227 # Not handled, appears un-used.
|
|
61
|
+
GET_M7_AUTO_OPERATION = 1228 # Not handled
|
|
62
|
+
EXTFILBOX = 1229 # Not handled
|
|
63
|
+
GET_EXTFILBOX_INFO = 1230 # Not handled
|
|
64
|
+
QUERY_PERIPHERALS = 1231
|
|
65
|
+
GET_LIGHT_STATUS = 1232
|
|
66
|
+
SET_LIGHT_STATUS = 1233
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AnycubicFunctionID(IntEnum):
|
|
70
|
+
AXLE_MOVEMENT = 1
|
|
71
|
+
FILE_MANAGER = 2
|
|
72
|
+
EXPOSURE_TEST = 3
|
|
73
|
+
LCD_PEER_VIDEO = 7
|
|
74
|
+
FDM_AXIS_MOVE = 13
|
|
75
|
+
FDM_PEER_VIDEO = 22
|
|
76
|
+
DEVICE_STARTUP_SELF_TEST = 26
|
|
77
|
+
PRINT_STARTUP_SELF_TEST = 27
|
|
78
|
+
AUTOMATIC_OPERATION = 28
|
|
79
|
+
RESIDUE_CLEAN = 29
|
|
80
|
+
NOVICE_GUIDE = 30
|
|
81
|
+
RELEASE_FILM = 31
|
|
82
|
+
TASK_MODE = 32
|
|
83
|
+
LCD_INTELLIGENT_MATERIALS_BOX = 33
|
|
84
|
+
LCD_AUTO_OUT_IN_MATERIALS = 34
|
|
85
|
+
M7PRO_AUTOMATIC_OPERATION = 35
|
|
86
|
+
AI_DETECTION = 36
|
|
87
|
+
AUTO_LEVELER = 37
|
|
88
|
+
VIBRATION_COMPENSATION = 38
|
|
89
|
+
TIME_LAPSE = 39
|
|
90
|
+
VIDEO_LIGHT = 40
|
|
91
|
+
BOX_LIGHT = 41
|
|
92
|
+
MULTI_COLOR_BOX = 2006
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AnycubicPrinterMaterialType(StrEnum):
|
|
96
|
+
FILAMENT = "Filament"
|
|
97
|
+
RESIN = "Resin"
|