flashforge-python-api 1.3.1__py3-none-any.whl → 1.3.3__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.
- flashforge/api/controls/control.py +16 -9
- flashforge/api/controls/creator5_palette.py +9 -6
- flashforge/api/controls/files.py +26 -8
- flashforge/api/controls/info.py +33 -9
- flashforge/api/controls/job_control.py +109 -71
- flashforge/api/controls/temp_control.py +14 -7
- flashforge/api/misc/redaction.py +88 -0
- flashforge/client.py +27 -10
- flashforge/discovery/discovery.py +10 -6
- flashforge/models/machine_info.py +40 -7
- flashforge/models/responses.py +22 -17
- flashforge/tcp/a3_client.py +27 -8
- flashforge/tcp/a4_client.py +5 -1
- flashforge/tcp/parsers/endstop_status.py +10 -6
- flashforge/tcp/parsers/location_info.py +3 -1
- flashforge/tcp/parsers/print_status.py +8 -3
- flashforge/tcp/parsers/printer_info.py +3 -1
- flashforge/tcp/parsers/temp_info.py +3 -1
- flashforge/tcp/parsers/thumbnail_info.py +19 -12
- flashforge/tcp/tcp_client.py +6 -5
- {flashforge_python_api-1.3.1.dist-info → flashforge_python_api-1.3.3.dist-info}/METADATA +1 -1
- flashforge_python_api-1.3.3.dist-info/RECORD +48 -0
- {flashforge_python_api-1.3.1.dist-info → flashforge_python_api-1.3.3.dist-info}/WHEEL +1 -1
- flashforge_python_api-1.3.1.dist-info/RECORD +0 -47
- {flashforge_python_api-1.3.1.dist-info → flashforge_python_api-1.3.3.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.3.1.dist-info → flashforge_python_api-1.3.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,6 +4,7 @@ FlashForge Python API - Job Control Module
|
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
6
|
import json
|
|
7
|
+
import logging
|
|
7
8
|
import re
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import TYPE_CHECKING, Any
|
|
@@ -19,12 +20,15 @@ from ...models.responses import (
|
|
|
19
20
|
Creator5UploadParams,
|
|
20
21
|
)
|
|
21
22
|
from ..constants.endpoints import Endpoints
|
|
23
|
+
from ..misc.redaction import redact_mapping
|
|
22
24
|
from ..network.utils import NetworkUtils, json_from_response
|
|
23
25
|
|
|
24
26
|
if TYPE_CHECKING:
|
|
25
27
|
from ...client import FlashForgeClient
|
|
26
28
|
from .control import Control
|
|
27
29
|
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
28
32
|
|
|
29
33
|
class JobControl:
|
|
30
34
|
"""
|
|
@@ -136,14 +140,18 @@ class JobControl:
|
|
|
136
140
|
file_path_obj = Path(file_path)
|
|
137
141
|
|
|
138
142
|
if not file_path_obj.exists():
|
|
139
|
-
|
|
143
|
+
logger.warning("upload_file: file not found at %s", file_path)
|
|
140
144
|
return False
|
|
141
145
|
|
|
142
146
|
file_size = file_path_obj.stat().st_size
|
|
143
147
|
file_name = file_path_obj.name
|
|
144
148
|
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
logger.debug(
|
|
150
|
+
"Starting upload for %s (size=%s, start=%s, level=%s)",
|
|
151
|
+
file_name,
|
|
152
|
+
file_size,
|
|
153
|
+
start_print,
|
|
154
|
+
level_before_print,
|
|
147
155
|
)
|
|
148
156
|
|
|
149
157
|
try:
|
|
@@ -159,16 +167,16 @@ class JobControl:
|
|
|
159
167
|
|
|
160
168
|
# Add additional headers for new firmware
|
|
161
169
|
if self._is_new_firmware_version():
|
|
162
|
-
|
|
170
|
+
logger.debug("Using new firmware headers for upload.")
|
|
163
171
|
custom_headers["flowCalibration"] = "false"
|
|
164
172
|
custom_headers["useMatlStation"] = "false"
|
|
165
173
|
custom_headers["gcodeToolCnt"] = "0"
|
|
166
174
|
# Base64 encode "[]" which is "W10="
|
|
167
175
|
custom_headers["materialMappings"] = "W10="
|
|
168
176
|
else:
|
|
169
|
-
|
|
177
|
+
logger.debug("Using old firmware headers for upload.")
|
|
170
178
|
|
|
171
|
-
|
|
179
|
+
logger.debug("Upload request headers: %s", redact_mapping(custom_headers))
|
|
172
180
|
|
|
173
181
|
# Create multipart form data
|
|
174
182
|
async with aiohttp.ClientSession() as session:
|
|
@@ -183,26 +191,31 @@ class JobControl:
|
|
|
183
191
|
data=data,
|
|
184
192
|
headers=custom_headers,
|
|
185
193
|
) as response:
|
|
186
|
-
|
|
194
|
+
logger.debug("Upload response status: %s", response.status)
|
|
187
195
|
|
|
188
196
|
if response.status != 200:
|
|
189
|
-
|
|
197
|
+
logger.warning(
|
|
198
|
+
"Upload failed: the printer responded with status %s",
|
|
199
|
+
response.status,
|
|
200
|
+
)
|
|
190
201
|
return False
|
|
191
202
|
|
|
192
203
|
result = await json_from_response(response)
|
|
193
|
-
|
|
204
|
+
logger.debug("Upload response data: %s", result)
|
|
194
205
|
|
|
195
206
|
if NetworkUtils.is_ok(result):
|
|
196
|
-
|
|
207
|
+
logger.debug("Upload successful according to the printer response.")
|
|
197
208
|
return True
|
|
198
209
|
else:
|
|
199
|
-
|
|
200
|
-
|
|
210
|
+
logger.warning(
|
|
211
|
+
"Upload failed: printer response code=%s, message=%s",
|
|
212
|
+
result.get("code"),
|
|
213
|
+
result.get("message"),
|
|
201
214
|
)
|
|
202
215
|
return False
|
|
203
216
|
|
|
204
217
|
except Exception as e:
|
|
205
|
-
|
|
218
|
+
logger.warning("upload_file error: %s", e)
|
|
206
219
|
return False
|
|
207
220
|
|
|
208
221
|
async def print_local_file(self, file_name: str, leveling_before_print: bool) -> bool:
|
|
@@ -252,7 +265,7 @@ class JobControl:
|
|
|
252
265
|
return NetworkUtils.is_ok(result)
|
|
253
266
|
|
|
254
267
|
except Exception as error:
|
|
255
|
-
|
|
268
|
+
logger.warning("print_local_file error: %s", error)
|
|
256
269
|
raise error
|
|
257
270
|
|
|
258
271
|
async def upload_file_ad5x(self, params: AD5XUploadParams) -> bool:
|
|
@@ -278,14 +291,19 @@ class JobControl:
|
|
|
278
291
|
# Validate file exists
|
|
279
292
|
file_path_obj = Path(params.file_path)
|
|
280
293
|
if not file_path_obj.exists():
|
|
281
|
-
|
|
294
|
+
logger.warning("upload_file_ad5x: file not found at %s", params.file_path)
|
|
282
295
|
return False
|
|
283
296
|
|
|
284
297
|
file_size = file_path_obj.stat().st_size
|
|
285
298
|
file_name = file_path_obj.name
|
|
286
299
|
|
|
287
|
-
|
|
288
|
-
|
|
300
|
+
logger.debug(
|
|
301
|
+
"Starting AD5X upload for %s (size=%s, start=%s, level=%s, tools=%s)",
|
|
302
|
+
file_name,
|
|
303
|
+
file_size,
|
|
304
|
+
params.start_print,
|
|
305
|
+
params.leveling_before_print,
|
|
306
|
+
len(params.material_mappings),
|
|
289
307
|
)
|
|
290
308
|
|
|
291
309
|
try:
|
|
@@ -310,7 +328,7 @@ class JobControl:
|
|
|
310
328
|
"Expect": "100-continue",
|
|
311
329
|
}
|
|
312
330
|
|
|
313
|
-
|
|
331
|
+
logger.debug("AD5X upload request headers: %s", redact_mapping(custom_headers))
|
|
314
332
|
|
|
315
333
|
# Create multipart form data
|
|
316
334
|
async with aiohttp.ClientSession() as session:
|
|
@@ -325,28 +343,33 @@ class JobControl:
|
|
|
325
343
|
data=data,
|
|
326
344
|
headers=custom_headers,
|
|
327
345
|
) as response:
|
|
328
|
-
|
|
346
|
+
logger.debug("AD5X upload response status: %s", response.status)
|
|
329
347
|
|
|
330
348
|
if response.status != 200:
|
|
331
|
-
|
|
332
|
-
|
|
349
|
+
logger.warning(
|
|
350
|
+
"AD5X upload failed: the printer responded with status %s",
|
|
351
|
+
response.status,
|
|
333
352
|
)
|
|
334
353
|
return False
|
|
335
354
|
|
|
336
355
|
result = await json_from_response(response)
|
|
337
|
-
|
|
356
|
+
logger.debug("AD5X upload response data: %s", result)
|
|
338
357
|
|
|
339
358
|
if NetworkUtils.is_ok(result):
|
|
340
|
-
|
|
359
|
+
logger.debug(
|
|
360
|
+
"AD5X upload successful according to the printer response."
|
|
361
|
+
)
|
|
341
362
|
return True
|
|
342
363
|
else:
|
|
343
|
-
|
|
344
|
-
|
|
364
|
+
logger.warning(
|
|
365
|
+
"AD5X upload failed: printer response code=%s, message=%s",
|
|
366
|
+
result.get("code"),
|
|
367
|
+
result.get("message"),
|
|
345
368
|
)
|
|
346
369
|
return False
|
|
347
370
|
|
|
348
371
|
except Exception as e:
|
|
349
|
-
|
|
372
|
+
logger.warning("upload_file_ad5x error: %s", e)
|
|
350
373
|
return False
|
|
351
374
|
|
|
352
375
|
async def start_ad5x_multi_color_job(self, params: AD5XLocalJobParams) -> bool:
|
|
@@ -371,7 +394,7 @@ class JobControl:
|
|
|
371
394
|
|
|
372
395
|
# Validate file name
|
|
373
396
|
if not params.file_name or params.file_name.strip() == "":
|
|
374
|
-
|
|
397
|
+
logger.warning("AD5X multi-color job: fileName cannot be empty.")
|
|
375
398
|
return False
|
|
376
399
|
|
|
377
400
|
# Create payload with AD5X-specific parameters
|
|
@@ -411,7 +434,7 @@ class JobControl:
|
|
|
411
434
|
return NetworkUtils.is_ok(result)
|
|
412
435
|
|
|
413
436
|
except Exception as error:
|
|
414
|
-
|
|
437
|
+
logger.warning("AD5X multi-color job error: %s", error)
|
|
415
438
|
raise error
|
|
416
439
|
|
|
417
440
|
async def start_ad5x_single_color_job(self, params: AD5XSingleColorJobParams) -> bool:
|
|
@@ -432,7 +455,7 @@ class JobControl:
|
|
|
432
455
|
|
|
433
456
|
# Validate file name
|
|
434
457
|
if not params.file_name or params.file_name.strip() == "":
|
|
435
|
-
|
|
458
|
+
logger.warning("AD5X single-color job: fileName cannot be empty.")
|
|
436
459
|
return False
|
|
437
460
|
|
|
438
461
|
# Create payload with AD5X-specific parameters for single-color printing
|
|
@@ -463,7 +486,7 @@ class JobControl:
|
|
|
463
486
|
return NetworkUtils.is_ok(result)
|
|
464
487
|
|
|
465
488
|
except Exception as error:
|
|
466
|
-
|
|
489
|
+
logger.warning("AD5X single-color job error: %s", error)
|
|
467
490
|
raise error
|
|
468
491
|
|
|
469
492
|
# --- Creator 5 / Creator 5 Pro ---
|
|
@@ -495,16 +518,21 @@ class JobControl:
|
|
|
495
518
|
file_path_obj = Path(params.file_path)
|
|
496
519
|
|
|
497
520
|
if not file_path_obj.exists():
|
|
498
|
-
|
|
521
|
+
logger.warning("upload_file_creator5: file not found at %s", params.file_path)
|
|
499
522
|
return False
|
|
500
523
|
|
|
501
524
|
file_size = file_path_obj.stat().st_size
|
|
502
525
|
file_name = file_path_obj.name
|
|
503
526
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
527
|
+
logger.debug(
|
|
528
|
+
"Starting Creator 5 upload for %s (size=%s, start=%s, level=%s, "
|
|
529
|
+
"matl_station=%s, tools=%s)",
|
|
530
|
+
file_name,
|
|
531
|
+
file_size,
|
|
532
|
+
params.start_print,
|
|
533
|
+
params.leveling_before_print,
|
|
534
|
+
params.use_matl_station,
|
|
535
|
+
params.gcode_tool_cnt,
|
|
508
536
|
)
|
|
509
537
|
|
|
510
538
|
try:
|
|
@@ -524,7 +552,7 @@ class JobControl:
|
|
|
524
552
|
"Expect": "100-continue",
|
|
525
553
|
}
|
|
526
554
|
|
|
527
|
-
|
|
555
|
+
logger.debug("Creator 5 upload request headers: %s", redact_mapping(custom_headers))
|
|
528
556
|
|
|
529
557
|
# Create multipart form data
|
|
530
558
|
async with aiohttp.ClientSession() as session:
|
|
@@ -539,30 +567,33 @@ class JobControl:
|
|
|
539
567
|
data=data,
|
|
540
568
|
headers=custom_headers,
|
|
541
569
|
) as response:
|
|
542
|
-
|
|
570
|
+
logger.debug("Creator 5 upload response status: %s", response.status)
|
|
543
571
|
|
|
544
572
|
if response.status != 200:
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
573
|
+
logger.warning(
|
|
574
|
+
"Creator 5 upload failed: the printer responded with status %s",
|
|
575
|
+
response.status,
|
|
548
576
|
)
|
|
549
577
|
return False
|
|
550
578
|
|
|
551
579
|
result = await json_from_response(response)
|
|
552
|
-
|
|
580
|
+
logger.debug("Creator 5 upload response data: %s", result)
|
|
553
581
|
|
|
554
582
|
if NetworkUtils.is_ok(result):
|
|
555
|
-
|
|
583
|
+
logger.debug(
|
|
584
|
+
"Creator 5 upload successful according to the printer response."
|
|
585
|
+
)
|
|
556
586
|
return True
|
|
557
587
|
|
|
558
|
-
|
|
559
|
-
"Creator 5
|
|
560
|
-
|
|
588
|
+
logger.warning(
|
|
589
|
+
"Creator 5 upload failed: printer response code=%s, message=%s",
|
|
590
|
+
result.get("code"),
|
|
591
|
+
result.get("message"),
|
|
561
592
|
)
|
|
562
593
|
return False
|
|
563
594
|
|
|
564
595
|
except Exception as e:
|
|
565
|
-
|
|
596
|
+
logger.warning("upload_file_creator5 error: %s", e)
|
|
566
597
|
return False
|
|
567
598
|
|
|
568
599
|
async def start_creator5_job(self, params: Creator5JobParams) -> bool:
|
|
@@ -591,7 +622,7 @@ class JobControl:
|
|
|
591
622
|
return False
|
|
592
623
|
|
|
593
624
|
if not params.file_name or params.file_name.strip() == "":
|
|
594
|
-
|
|
625
|
+
logger.warning("Creator 5 job: fileName cannot be empty.")
|
|
595
626
|
return False
|
|
596
627
|
|
|
597
628
|
has_mappings = params.material_mappings is not None and len(params.material_mappings) > 0
|
|
@@ -634,7 +665,7 @@ class JobControl:
|
|
|
634
665
|
return NetworkUtils.is_ok(result)
|
|
635
666
|
|
|
636
667
|
except Exception as error:
|
|
637
|
-
|
|
668
|
+
logger.warning("Creator 5 job error: %s", error)
|
|
638
669
|
raise error
|
|
639
670
|
|
|
640
671
|
def _validate_material_station_printer(self) -> bool:
|
|
@@ -648,9 +679,8 @@ class JobControl:
|
|
|
648
679
|
True if the printer supports material mappings, False otherwise.
|
|
649
680
|
"""
|
|
650
681
|
if not self.client.is_ad5x and not self.client.is_creator5:
|
|
651
|
-
|
|
652
|
-
"Material-station job
|
|
653
|
-
"Creator 5 series printer"
|
|
682
|
+
logger.warning(
|
|
683
|
+
"Material-station job: this method requires an AD5X or Creator 5 series printer."
|
|
654
684
|
)
|
|
655
685
|
return False
|
|
656
686
|
return True
|
|
@@ -698,47 +728,53 @@ class JobControl:
|
|
|
698
728
|
if not material_mappings or len(material_mappings) == 0:
|
|
699
729
|
if allow_empty:
|
|
700
730
|
return True
|
|
701
|
-
|
|
702
|
-
f"{label} error: materialMappings array cannot be empty for multi-color jobs"
|
|
703
|
-
)
|
|
731
|
+
logger.warning("%s: materialMappings cannot be empty for multi-color jobs.", label)
|
|
704
732
|
return False
|
|
705
733
|
|
|
706
734
|
if len(material_mappings) > 4:
|
|
707
|
-
|
|
735
|
+
logger.warning("%s: a maximum of 4 material mappings is allowed.", label)
|
|
708
736
|
return False
|
|
709
737
|
|
|
710
738
|
hex_color_regex = re.compile(r"^#[0-9A-Fa-f]{6}$")
|
|
711
739
|
|
|
712
740
|
for i, mapping in enumerate(material_mappings):
|
|
713
741
|
if mapping.tool_id < 0 or mapping.tool_id > 3:
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
742
|
+
logger.warning(
|
|
743
|
+
"%s: toolId must be between 0-3, got %s at index %s",
|
|
744
|
+
label,
|
|
745
|
+
mapping.tool_id,
|
|
746
|
+
i,
|
|
717
747
|
)
|
|
718
748
|
return False
|
|
719
749
|
|
|
720
750
|
if mapping.slot_id < 1 or mapping.slot_id > 4:
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
751
|
+
logger.warning(
|
|
752
|
+
"%s: slotId must be between 1-4, got %s at index %s",
|
|
753
|
+
label,
|
|
754
|
+
mapping.slot_id,
|
|
755
|
+
i,
|
|
724
756
|
)
|
|
725
757
|
return False
|
|
726
758
|
|
|
727
759
|
if not mapping.material_name or mapping.material_name.strip() == "":
|
|
728
|
-
|
|
760
|
+
logger.warning("%s: materialName cannot be empty at index %s", label, i)
|
|
729
761
|
return False
|
|
730
762
|
|
|
731
763
|
if not hex_color_regex.match(mapping.tool_material_color):
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
764
|
+
logger.warning(
|
|
765
|
+
"%s: toolMaterialColor must be in #RRGGBB format, got %s at index %s",
|
|
766
|
+
label,
|
|
767
|
+
mapping.tool_material_color,
|
|
768
|
+
i,
|
|
735
769
|
)
|
|
736
770
|
return False
|
|
737
771
|
|
|
738
772
|
if not hex_color_regex.match(mapping.slot_material_color):
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
773
|
+
logger.warning(
|
|
774
|
+
"%s: slotMaterialColor must be in #RRGGBB format, got %s at index %s",
|
|
775
|
+
label,
|
|
776
|
+
mapping.slot_material_color,
|
|
777
|
+
i,
|
|
742
778
|
)
|
|
743
779
|
return False
|
|
744
780
|
|
|
@@ -752,7 +788,7 @@ class JobControl:
|
|
|
752
788
|
True if the printer is AD5X, false otherwise
|
|
753
789
|
"""
|
|
754
790
|
if not self.client.is_ad5x:
|
|
755
|
-
|
|
791
|
+
logger.warning("AD5X job: this method can only be used with AD5X printers.")
|
|
756
792
|
return False
|
|
757
793
|
return True
|
|
758
794
|
|
|
@@ -783,8 +819,10 @@ class JobControl:
|
|
|
783
819
|
json_string = json.dumps(json_array)
|
|
784
820
|
return base64.b64encode(json_string.encode("utf-8")).decode("utf-8")
|
|
785
821
|
except Exception as error:
|
|
786
|
-
|
|
787
|
-
raise Exception(
|
|
822
|
+
logger.warning("Failed to encode the material mappings to base64: %s", error)
|
|
823
|
+
raise Exception(
|
|
824
|
+
"Failed to encode material mappings for upload"
|
|
825
|
+
) from error # noqa: B904
|
|
788
826
|
|
|
789
827
|
def _validate_material_mappings(self, material_mappings: list[AD5XMaterialMapping]) -> bool:
|
|
790
828
|
"""
|
|
@@ -6,6 +6,7 @@ direct TCP G-code/M-code commands; HTTP-only printers (Creator 5 / 5 Pro, no TCP
|
|
|
6
6
|
channel) use the HTTP ``temperatureCtl_cmd`` instead.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import logging
|
|
9
10
|
from typing import TYPE_CHECKING
|
|
10
11
|
|
|
11
12
|
from ..constants.commands import Commands
|
|
@@ -14,6 +15,8 @@ if TYPE_CHECKING:
|
|
|
14
15
|
from ...client import FlashForgeClient
|
|
15
16
|
from ...tcp.ff_client import FlashForgeClient as TcpClient
|
|
16
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
# Sentinel value for `temperatureCtl_cmd` meaning "leave this heater unchanged"
|
|
19
22
|
# (partial update). Sending a real 0 / -100 would turn the heater off.
|
|
@@ -106,7 +109,9 @@ class TempControl:
|
|
|
106
109
|
The array, or None if ``tool_index`` is out of range.
|
|
107
110
|
"""
|
|
108
111
|
if not isinstance(tool_index, int) or tool_index < 0 or tool_index >= NOZZLE_COUNT:
|
|
109
|
-
|
|
112
|
+
logger.warning(
|
|
113
|
+
"TempControl: tool_index %s out of range (0-%s).", tool_index, NOZZLE_COUNT - 1
|
|
114
|
+
)
|
|
110
115
|
return None
|
|
111
116
|
nozzles = [TEMP_NO_CHANGE] * NOZZLE_COUNT
|
|
112
117
|
nozzles[tool_index] = value
|
|
@@ -145,7 +150,7 @@ class TempControl:
|
|
|
145
150
|
True if the command is acknowledged, False otherwise.
|
|
146
151
|
"""
|
|
147
152
|
if len(temps) != NOZZLE_COUNT:
|
|
148
|
-
|
|
153
|
+
logger.warning("set_tool_temps: expected %s temps, got %s.", NOZZLE_COUNT, len(temps))
|
|
149
154
|
return False
|
|
150
155
|
return await self._send_http_temp_command(nozzles=list(temps))
|
|
151
156
|
|
|
@@ -258,7 +263,9 @@ class TempControl:
|
|
|
258
263
|
True if the command is acknowledged, False otherwise.
|
|
259
264
|
"""
|
|
260
265
|
if not self.client.is_creator5 and not self.client.is_creator5_pro:
|
|
261
|
-
|
|
266
|
+
logger.warning(
|
|
267
|
+
"set_chamber_temp: the chamber heater is only available on the Creator 5 series."
|
|
268
|
+
)
|
|
262
269
|
return False
|
|
263
270
|
return await self._send_http_temp_command(chamber=temp)
|
|
264
271
|
|
|
@@ -270,8 +277,8 @@ class TempControl:
|
|
|
270
277
|
True if the command is acknowledged, False otherwise.
|
|
271
278
|
"""
|
|
272
279
|
if not self.client.is_creator5 and not self.client.is_creator5_pro:
|
|
273
|
-
|
|
274
|
-
"cancel_chamber_temp
|
|
280
|
+
logger.warning(
|
|
281
|
+
"cancel_chamber_temp: the chamber heater is only available on the Creator 5 series."
|
|
275
282
|
)
|
|
276
283
|
return False
|
|
277
284
|
return await self._send_http_temp_command(chamber=TEMP_OFF)
|
|
@@ -294,8 +301,8 @@ class TempControl:
|
|
|
294
301
|
or HTTP-only (no TCP polling channel).
|
|
295
302
|
"""
|
|
296
303
|
if self.client.http_only:
|
|
297
|
-
|
|
298
|
-
"wait_for_part_cool
|
|
304
|
+
logger.warning(
|
|
305
|
+
"wait_for_part_cool is unavailable over an HTTP-only connection; "
|
|
299
306
|
"poll info.get() instead."
|
|
300
307
|
)
|
|
301
308
|
return False
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlashForge Python API - Log redaction helpers.
|
|
3
|
+
|
|
4
|
+
Debug logs get pasted into bug reports verbatim, so anything that identifies or
|
|
5
|
+
grants control of a printer is masked before it can be formatted into a log
|
|
6
|
+
record. The key set mirrors what ``ff-5mp-hass`` already strips from its
|
|
7
|
+
diagnostics download, so the two surfaces cannot disagree about what is safe to
|
|
8
|
+
share.
|
|
9
|
+
|
|
10
|
+
- ``serialNumber`` / ``checkCode``: together these are full control of the
|
|
11
|
+
printer over the LAN API.
|
|
12
|
+
- ``flashRegisterCode`` / ``polarRegisterCode``: cloud-account registration
|
|
13
|
+
codes.
|
|
14
|
+
- ``macAddr`` / ``ipAddr``: identify the machine and its network.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
REDACTED = "<redacted>"
|
|
20
|
+
|
|
21
|
+
# Both the firmware's camelCase aliases and the library's snake_case field
|
|
22
|
+
# names, since payloads are logged in either form depending on the call site.
|
|
23
|
+
SENSITIVE_KEYS = frozenset(
|
|
24
|
+
{
|
|
25
|
+
"serialNumber",
|
|
26
|
+
"serial_number",
|
|
27
|
+
"checkCode",
|
|
28
|
+
"check_code",
|
|
29
|
+
"macAddr",
|
|
30
|
+
"mac_addr",
|
|
31
|
+
"mac_address",
|
|
32
|
+
"ipAddr",
|
|
33
|
+
"ip_addr",
|
|
34
|
+
"ip_address",
|
|
35
|
+
"flashRegisterCode",
|
|
36
|
+
"flash_register_code",
|
|
37
|
+
"flash_cloud_register_code",
|
|
38
|
+
"polarRegisterCode",
|
|
39
|
+
"polar_register_code",
|
|
40
|
+
"polar_cloud_register_code",
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _redact_value(value: Any) -> Any:
|
|
46
|
+
"""Recurse into nested containers so a sensitive key cannot hide one level down."""
|
|
47
|
+
if isinstance(value, dict):
|
|
48
|
+
return redact_mapping(value)
|
|
49
|
+
if isinstance(value, list):
|
|
50
|
+
return [_redact_value(item) for item in value]
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def redact_mapping(mapping: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
"""Return a copy of ``mapping`` with every sensitive value masked.
|
|
56
|
+
|
|
57
|
+
The input is never mutated - callers are usually about to send it.
|
|
58
|
+
"""
|
|
59
|
+
return {
|
|
60
|
+
key: (REDACTED if key in SENSITIVE_KEYS else _redact_value(value))
|
|
61
|
+
for key, value in mapping.items()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def redact_model(model: Any) -> Any:
|
|
66
|
+
"""Return a log-safe representation of a Pydantic model (or anything else).
|
|
67
|
+
|
|
68
|
+
Falls back to ``repr`` for objects that cannot be dumped, and to a plain
|
|
69
|
+
``<unloggable>`` if even that raises - a redaction helper must never be the
|
|
70
|
+
reason an error path fails.
|
|
71
|
+
"""
|
|
72
|
+
if model is None:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
dump = getattr(model, "model_dump", None)
|
|
76
|
+
if callable(dump):
|
|
77
|
+
try:
|
|
78
|
+
return redact_mapping(dump())
|
|
79
|
+
except Exception: # noqa: BLE001 - logging must not raise
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
if isinstance(model, dict):
|
|
83
|
+
return redact_mapping(model)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
return repr(model)
|
|
87
|
+
except Exception: # noqa: BLE001
|
|
88
|
+
return "<unloggable>"
|