flashforge-python-api 1.0.2__py3-none-any.whl → 1.2.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.
Files changed (43) hide show
  1. flashforge/__init__.py +70 -29
  2. flashforge/api/__init__.py +1 -0
  3. flashforge/api/constants/__init__.py +1 -0
  4. flashforge/api/constants/commands.py +1 -0
  5. flashforge/api/constants/endpoints.py +7 -0
  6. flashforge/api/controls/__init__.py +1 -0
  7. flashforge/api/controls/control.py +64 -61
  8. flashforge/api/controls/files.py +28 -26
  9. flashforge/api/controls/info.py +84 -85
  10. flashforge/api/controls/job_control.py +120 -82
  11. flashforge/api/controls/temp_control.py +14 -11
  12. flashforge/api/misc/__init__.py +1 -1
  13. flashforge/api/network/__init__.py +2 -1
  14. flashforge/api/network/utils.py +7 -8
  15. flashforge/client.py +185 -64
  16. flashforge/discovery/__init__.py +30 -3
  17. flashforge/discovery/discovery.py +641 -308
  18. flashforge/models/__init__.py +11 -10
  19. flashforge/models/machine_info.py +225 -100
  20. flashforge/models/responses.py +103 -43
  21. flashforge/tcp/__init__.py +30 -17
  22. flashforge/tcp/a3_client.py +408 -0
  23. flashforge/tcp/a4_client.py +300 -0
  24. flashforge/tcp/ff_client.py +93 -90
  25. flashforge/tcp/gcode/__init__.py +4 -2
  26. flashforge/tcp/gcode/a3_gcode_controller.py +109 -0
  27. flashforge/tcp/gcode/gcode_controller.py +48 -36
  28. flashforge/tcp/gcode/gcodes.py +7 -1
  29. flashforge/tcp/parsers/__init__.py +11 -11
  30. flashforge/tcp/parsers/endstop_status.py +103 -132
  31. flashforge/tcp/parsers/location_info.py +26 -13
  32. flashforge/tcp/parsers/print_status.py +49 -56
  33. flashforge/tcp/parsers/printer_info.py +38 -48
  34. flashforge/tcp/parsers/temp_info.py +46 -47
  35. flashforge/tcp/parsers/thumbnail_info.py +65 -40
  36. flashforge/tcp/tcp_client.py +296 -293
  37. flashforge_python_api-1.2.0.dist-info/METADATA +133 -0
  38. flashforge_python_api-1.2.0.dist-info/RECORD +46 -0
  39. {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/WHEEL +1 -1
  40. flashforge_python_api-1.0.2.dist-info/METADATA +0 -284
  41. flashforge_python_api-1.0.2.dist-info/RECORD +0 -43
  42. {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/entry_points.txt +0 -0
  43. {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,21 @@
1
1
  """
2
2
  FlashForge Python API - Job Control Module
3
3
  """
4
+
4
5
  import base64
5
6
  import json
6
7
  import re
7
8
  from pathlib import Path
8
- from typing import TYPE_CHECKING, Optional
9
+ from typing import TYPE_CHECKING
9
10
 
10
11
  import aiohttp
11
12
 
12
- from ...models.responses import AD5XMaterialMapping, AD5XUploadParams, AD5XLocalJobParams, AD5XSingleColorJobParams
13
+ from ...models.responses import (
14
+ AD5XLocalJobParams,
15
+ AD5XMaterialMapping,
16
+ AD5XSingleColorJobParams,
17
+ AD5XUploadParams,
18
+ )
13
19
  from ..constants.endpoints import Endpoints
14
20
  from ..network.utils import NetworkUtils
15
21
 
@@ -28,12 +34,12 @@ class JobControl:
28
34
  def __init__(self, client: "FlashForgeClient"):
29
35
  """
30
36
  Creates an instance of the JobControl class.
31
-
37
+
32
38
  Args:
33
39
  client: The FlashForgeClient instance used for communication with the printer.
34
40
  """
35
41
  self.client = client
36
- self._control: Optional[Control] = None
42
+ self._control: Control | None = None
37
43
 
38
44
  @property
39
45
  def control(self) -> "Control":
@@ -45,7 +51,7 @@ class JobControl:
45
51
  async def pause_print_job(self) -> bool:
46
52
  """
47
53
  Pauses the current print job.
48
-
54
+
49
55
  Returns:
50
56
  True if the command is successful, False otherwise.
51
57
  """
@@ -54,7 +60,7 @@ class JobControl:
54
60
  async def resume_print_job(self) -> bool:
55
61
  """
56
62
  Resumes a paused print job.
57
-
63
+
58
64
  Returns:
59
65
  True if the command is successful, False otherwise.
60
66
  """
@@ -63,7 +69,7 @@ class JobControl:
63
69
  async def cancel_print_job(self) -> bool:
64
70
  """
65
71
  Cancels the current print job.
66
-
72
+
67
73
  Returns:
68
74
  True if the command is successful, False otherwise.
69
75
  """
@@ -73,16 +79,16 @@ class JobControl:
73
79
  """
74
80
  Checks if the printer's firmware version is 3.1.3 or newer.
75
81
  This is used to determine which API payload format to use for certain commands.
76
-
82
+
77
83
  Returns:
78
84
  True if the firmware is new (>= 3.1.3), False otherwise or if version cannot be determined.
79
85
  """
80
86
  try:
81
- current_version = self.client.firmware_ver.split('.')
87
+ current_version = self.client.firmware_ver.split(".")
82
88
  min_version = [3, 1, 3]
83
89
 
84
90
  for i in range(3):
85
- current = int(current_version[i] if i < len(current_version) else '0')
91
+ current = int(current_version[i] if i < len(current_version) else "0")
86
92
  if current > min_version[i]:
87
93
  return True
88
94
  if current < min_version[i]:
@@ -95,26 +101,26 @@ class JobControl:
95
101
  async def clear_platform(self) -> bool:
96
102
  """
97
103
  Sends a command to clear the printer's build platform.
98
-
104
+
99
105
  Returns:
100
106
  True if the command is successful, False otherwise.
101
107
  """
102
- args = {
103
- "action": "setClearPlatform"
104
- }
108
+ args = {"action": "setClearPlatform"}
105
109
 
106
110
  return await self.control.send_control_command("stateCtrl_cmd", args)
107
111
 
108
- async def upload_file(self, file_path: str, start_print: bool, level_before_print: bool) -> bool:
112
+ async def upload_file(
113
+ self, file_path: str, start_print: bool, level_before_print: bool
114
+ ) -> bool:
109
115
  """
110
116
  Uploads a G-code or 3MF file to the printer and optionally starts printing.
111
117
  It handles different API requirements based on the printer's firmware version.
112
-
118
+
113
119
  Args:
114
120
  file_path: The local path to the G-code or 3MF file to upload.
115
121
  start_print: If True, the printer will start printing the file immediately after upload.
116
122
  level_before_print: If True, the printer will perform bed leveling before starting the print.
117
-
123
+
118
124
  Returns:
119
125
  True if the file upload (and optional print start) is successful, False otherwise.
120
126
  """
@@ -127,27 +133,29 @@ class JobControl:
127
133
  file_size = file_path_obj.stat().st_size
128
134
  file_name = file_path_obj.name
129
135
 
130
- print(f"Starting upload for {file_name}, Size: {file_size}, Start: {start_print}, Level: {level_before_print}")
136
+ print(
137
+ f"Starting upload for {file_name}, Size: {file_size}, Start: {start_print}, Level: {level_before_print}"
138
+ )
131
139
 
132
140
  try:
133
141
  # Prepare the custom HTTP headers with metadata
134
142
  custom_headers = {
135
- 'serialNumber': self.client.serial_number,
136
- 'checkCode': self.client.check_code,
137
- 'fileSize': str(file_size),
138
- 'printNow': str(start_print).lower(),
139
- 'levelingBeforePrint': str(level_before_print).lower(),
140
- 'Expect': '100-continue'
143
+ "serialNumber": self.client.serial_number,
144
+ "checkCode": self.client.check_code,
145
+ "fileSize": str(file_size),
146
+ "printNow": str(start_print).lower(),
147
+ "levelingBeforePrint": str(level_before_print).lower(),
148
+ "Expect": "100-continue",
141
149
  }
142
150
 
143
151
  # Add additional headers for new firmware
144
152
  if self._is_new_firmware_version():
145
153
  print("Using new firmware headers for upload.")
146
- custom_headers['flowCalibration'] = 'false'
147
- custom_headers['useMatlStation'] = 'false'
148
- custom_headers['gcodeToolCnt'] = '0'
154
+ custom_headers["flowCalibration"] = "false"
155
+ custom_headers["useMatlStation"] = "false"
156
+ custom_headers["gcodeToolCnt"] = "0"
149
157
  # Base64 encode "[]" which is "W10="
150
- custom_headers['materialMappings'] = 'W10='
158
+ custom_headers["materialMappings"] = "W10="
151
159
  else:
152
160
  print("Using old firmware headers for upload.")
153
161
 
@@ -155,14 +163,16 @@ class JobControl:
155
163
 
156
164
  # Create multipart form data
157
165
  async with aiohttp.ClientSession() as session:
158
- with open(file_path, 'rb') as f:
166
+ with open(file_path, "rb") as f:
159
167
  data = aiohttp.FormData()
160
- data.add_field('gcodeFile', f, filename=file_name, content_type='application/octet-stream')
168
+ data.add_field(
169
+ "gcodeFile", f, filename=file_name, content_type="application/octet-stream"
170
+ )
161
171
 
162
172
  async with session.post(
163
173
  self.client.get_endpoint(Endpoints.UPLOAD_FILE),
164
174
  data=data,
165
- headers=custom_headers
175
+ headers=custom_headers,
166
176
  ) as response:
167
177
  print(f"Upload Response Status: {response.status}")
168
178
 
@@ -178,15 +188,18 @@ class JobControl:
178
188
  # Fallback: manually parse as JSON if Content-Type is malformed
179
189
  text = await response.text()
180
190
  import json
191
+
181
192
  result = json.loads(text)
182
-
193
+
183
194
  print("Upload Response Data:", result)
184
195
 
185
196
  if NetworkUtils.is_ok(result):
186
197
  print("Upload successful according to printer response.")
187
198
  return True
188
199
  else:
189
- print(f"Upload failed: Printer response code={result.get('code')}, message={result.get('message')}")
200
+ print(
201
+ f"Upload failed: Printer response code={result.get('code')}, message={result.get('message')}"
202
+ )
190
203
  return False
191
204
 
192
205
  except Exception as e:
@@ -197,11 +210,11 @@ class JobControl:
197
210
  """
198
211
  Starts printing a file that is already stored locally on the printer.
199
212
  It handles different API payload formats based on the printer's firmware version.
200
-
213
+
201
214
  Args:
202
215
  file_name: The name of the file on the printer (e.g., "my_model.gcode") to print.
203
216
  leveling_before_print: If True, the printer will perform bed leveling before starting the print.
204
-
217
+
205
218
  Returns:
206
219
  True if the print command is successfully sent and acknowledged, False otherwise.
207
220
  """
@@ -215,7 +228,7 @@ class JobControl:
215
228
  "flowCalibration": False,
216
229
  "useMatlStation": False,
217
230
  "gcodeToolCnt": 0,
218
- "materialMappings": [] # Empty array for materialMappings
231
+ "materialMappings": [], # Empty array for materialMappings
219
232
  }
220
233
  else:
221
234
  # Old format for firmware < 3.1.3
@@ -223,7 +236,7 @@ class JobControl:
223
236
  "serialNumber": self.client.serial_number,
224
237
  "checkCode": self.client.check_code,
225
238
  "fileName": file_name,
226
- "levelingBeforePrint": leveling_before_print
239
+ "levelingBeforePrint": leveling_before_print,
227
240
  }
228
241
 
229
242
  try:
@@ -231,7 +244,7 @@ class JobControl:
231
244
  async with session.post(
232
245
  self.client.get_endpoint(Endpoints.GCODE_PRINT),
233
246
  json=payload,
234
- headers={"Content-Type": "application/json"}
247
+ headers={"Content-Type": "application/json"},
235
248
  ) as response:
236
249
  if response.status != 200:
237
250
  return False
@@ -244,8 +257,9 @@ class JobControl:
244
257
  # Fallback: manually parse as JSON if Content-Type is malformed
245
258
  text = await response.text()
246
259
  import json
260
+
247
261
  result = json.loads(text)
248
-
262
+
249
263
  return NetworkUtils.is_ok(result)
250
264
 
251
265
  except Exception as error:
@@ -281,45 +295,53 @@ class JobControl:
281
295
  file_size = file_path_obj.stat().st_size
282
296
  file_name = file_path_obj.name
283
297
 
284
- print(f"Starting AD5X upload for {file_name}, Size: {file_size}, Start: {params.start_print}, Level: {params.leveling_before_print}, Tools: {len(params.material_mappings)}")
298
+ print(
299
+ f"Starting AD5X upload for {file_name}, Size: {file_size}, Start: {params.start_print}, Level: {params.leveling_before_print}, Tools: {len(params.material_mappings)}"
300
+ )
285
301
 
286
302
  try:
287
303
  # Encode material mappings to base64
288
- material_mappings_base64 = self._encode_material_mappings_to_base64(params.material_mappings)
304
+ material_mappings_base64 = self._encode_material_mappings_to_base64(
305
+ params.material_mappings
306
+ )
289
307
 
290
308
  # Prepare AD5X-specific HTTP headers
291
309
  custom_headers = {
292
- 'serialNumber': self.client.serial_number,
293
- 'checkCode': self.client.check_code,
294
- 'fileSize': str(file_size),
295
- 'printNow': str(params.start_print).lower(),
296
- 'levelingBeforePrint': str(params.leveling_before_print).lower(),
297
- 'flowCalibration': str(params.flow_calibration).lower(),
298
- 'firstLayerInspection': str(params.first_layer_inspection).lower(),
299
- 'timeLapseVideo': str(params.time_lapse_video).lower(),
300
- 'useMatlStation': 'true', # Always true for AD5X uploads with material mappings
301
- 'gcodeToolCnt': str(len(params.material_mappings)),
302
- 'materialMappings': material_mappings_base64,
303
- 'Expect': '100-continue'
310
+ "serialNumber": self.client.serial_number,
311
+ "checkCode": self.client.check_code,
312
+ "fileSize": str(file_size),
313
+ "printNow": str(params.start_print).lower(),
314
+ "levelingBeforePrint": str(params.leveling_before_print).lower(),
315
+ "flowCalibration": str(params.flow_calibration).lower(),
316
+ "firstLayerInspection": str(params.first_layer_inspection).lower(),
317
+ "timeLapseVideo": str(params.time_lapse_video).lower(),
318
+ "useMatlStation": "true", # Always true for AD5X uploads with material mappings
319
+ "gcodeToolCnt": str(len(params.material_mappings)),
320
+ "materialMappings": material_mappings_base64,
321
+ "Expect": "100-continue",
304
322
  }
305
323
 
306
324
  print("AD5X Upload Request Headers:", custom_headers)
307
325
 
308
326
  # Create multipart form data
309
327
  async with aiohttp.ClientSession() as session:
310
- with open(params.file_path, 'rb') as f:
328
+ with open(params.file_path, "rb") as f:
311
329
  data = aiohttp.FormData()
312
- data.add_field('gcodeFile', f, filename=file_name, content_type='application/octet-stream')
330
+ data.add_field(
331
+ "gcodeFile", f, filename=file_name, content_type="application/octet-stream"
332
+ )
313
333
 
314
334
  async with session.post(
315
335
  self.client.get_endpoint(Endpoints.UPLOAD_FILE),
316
336
  data=data,
317
- headers=custom_headers
337
+ headers=custom_headers,
318
338
  ) as response:
319
339
  print(f"AD5X Upload Response Status: {response.status}")
320
340
 
321
341
  if response.status != 200:
322
- print(f"AD5X Upload failed: Printer responded with status {response.status}")
342
+ print(
343
+ f"AD5X Upload failed: Printer responded with status {response.status}"
344
+ )
323
345
  return False
324
346
 
325
347
  # Fix for FlashForge printer's malformed Content-Type header
@@ -335,7 +357,9 @@ class JobControl:
335
357
  print("AD5X Upload successful according to printer response.")
336
358
  return True
337
359
  else:
338
- print(f"AD5X Upload failed: Printer response code={result.get('code')}, message={result.get('message')}")
360
+ print(
361
+ f"AD5X Upload failed: Printer response code={result.get('code')}, message={result.get('message')}"
362
+ )
339
363
  return False
340
364
 
341
365
  except Exception as e:
@@ -363,8 +387,8 @@ class JobControl:
363
387
  return False
364
388
 
365
389
  # Validate file name
366
- if not params.file_name or params.file_name.strip() == '':
367
- print('AD5X Multi-Color Job error: fileName cannot be empty')
390
+ if not params.file_name or params.file_name.strip() == "":
391
+ print("AD5X Multi-Color Job error: fileName cannot be empty")
368
392
  return False
369
393
 
370
394
  # Create payload with AD5X-specific parameters
@@ -384,10 +408,10 @@ class JobControl:
384
408
  "slotId": m.slot_id,
385
409
  "materialName": m.material_name,
386
410
  "toolMaterialColor": m.tool_material_color,
387
- "slotMaterialColor": m.slot_material_color
411
+ "slotMaterialColor": m.slot_material_color,
388
412
  }
389
413
  for m in params.material_mappings
390
- ]
414
+ ],
391
415
  }
392
416
 
393
417
  try:
@@ -395,7 +419,7 @@ class JobControl:
395
419
  async with session.post(
396
420
  self.client.get_endpoint(Endpoints.GCODE_PRINT),
397
421
  json=payload,
398
- headers={"Content-Type": "application/json"}
422
+ headers={"Content-Type": "application/json"},
399
423
  ) as response:
400
424
  if response.status != 200:
401
425
  return False
@@ -430,8 +454,8 @@ class JobControl:
430
454
  return False
431
455
 
432
456
  # Validate file name
433
- if not params.file_name or params.file_name.strip() == '':
434
- print('AD5X Single-Color Job error: fileName cannot be empty')
457
+ if not params.file_name or params.file_name.strip() == "":
458
+ print("AD5X Single-Color Job error: fileName cannot be empty")
435
459
  return False
436
460
 
437
461
  # Create payload with AD5X-specific parameters for single-color printing
@@ -445,7 +469,7 @@ class JobControl:
445
469
  "timeLapseVideo": False,
446
470
  "useMatlStation": False, # Set to false for single-color jobs
447
471
  "gcodeToolCnt": 0, # Set to 0 for single-color jobs
448
- "materialMappings": [] # Empty array for single-color jobs
472
+ "materialMappings": [], # Empty array for single-color jobs
449
473
  }
450
474
 
451
475
  try:
@@ -453,7 +477,7 @@ class JobControl:
453
477
  async with session.post(
454
478
  self.client.get_endpoint(Endpoints.GCODE_PRINT),
455
479
  json=payload,
456
- headers={"Content-Type": "application/json"}
480
+ headers={"Content-Type": "application/json"},
457
481
  ) as response:
458
482
  if response.status != 200:
459
483
  return False
@@ -479,11 +503,13 @@ class JobControl:
479
503
  True if the printer is AD5X, false otherwise
480
504
  """
481
505
  if not self.client.is_ad5x:
482
- print('AD5X Job error: This method can only be used with AD5X printers')
506
+ print("AD5X Job error: This method can only be used with AD5X printers")
483
507
  return False
484
508
  return True
485
509
 
486
- def _encode_material_mappings_to_base64(self, material_mappings: list[AD5XMaterialMapping]) -> str:
510
+ def _encode_material_mappings_to_base64(
511
+ self, material_mappings: list[AD5XMaterialMapping]
512
+ ) -> str:
487
513
  """
488
514
  Encodes material mappings array to base64 string for HTTP headers.
489
515
  Converts AD5XMaterialMapping array to JSON and then to base64 encoding.
@@ -501,15 +527,15 @@ class JobControl:
501
527
  "slotId": m.slot_id,
502
528
  "materialName": m.material_name,
503
529
  "toolMaterialColor": m.tool_material_color,
504
- "slotMaterialColor": m.slot_material_color
530
+ "slotMaterialColor": m.slot_material_color,
505
531
  }
506
532
  for m in material_mappings
507
533
  ]
508
534
  json_string = json.dumps(json_array)
509
- return base64.b64encode(json_string.encode('utf-8')).decode('utf-8')
535
+ return base64.b64encode(json_string.encode("utf-8")).decode("utf-8")
510
536
  except Exception as error:
511
- print('Failed to encode material mappings to base64:', error)
512
- raise Exception('Failed to encode material mappings for upload')
537
+ print("Failed to encode material mappings to base64:", error)
538
+ raise Exception("Failed to encode material mappings for upload") from error # noqa: B904
513
539
 
514
540
  def _validate_material_mappings(self, material_mappings: list[AD5XMaterialMapping]) -> bool:
515
541
  """
@@ -523,39 +549,51 @@ class JobControl:
523
549
  True if all mappings are valid, false otherwise
524
550
  """
525
551
  if not material_mappings or len(material_mappings) == 0:
526
- print('Material mappings validation error: materialMappings array cannot be empty for multi-color jobs')
552
+ print(
553
+ "Material mappings validation error: materialMappings array cannot be empty for multi-color jobs"
554
+ )
527
555
  return False
528
556
 
529
557
  if len(material_mappings) > 4:
530
- print('Material mappings validation error: Maximum 4 material mappings allowed')
558
+ print("Material mappings validation error: Maximum 4 material mappings allowed")
531
559
  return False
532
560
 
533
- hex_color_regex = re.compile(r'^#[0-9A-Fa-f]{6}$')
561
+ hex_color_regex = re.compile(r"^#[0-9A-Fa-f]{6}$")
534
562
 
535
563
  for i, mapping in enumerate(material_mappings):
536
564
  # Validate toolId (0-3)
537
565
  if mapping.tool_id < 0 or mapping.tool_id > 3:
538
- print(f'Material mappings validation error: toolId must be between 0-3, got {mapping.tool_id} at index {i}')
566
+ print(
567
+ f"Material mappings validation error: toolId must be between 0-3, got {mapping.tool_id} at index {i}"
568
+ )
539
569
  return False
540
570
 
541
571
  # Validate slotId (1-4)
542
572
  if mapping.slot_id < 1 or mapping.slot_id > 4:
543
- print(f'Material mappings validation error: slotId must be between 1-4, got {mapping.slot_id} at index {i}')
573
+ print(
574
+ f"Material mappings validation error: slotId must be between 1-4, got {mapping.slot_id} at index {i}"
575
+ )
544
576
  return False
545
577
 
546
578
  # Validate materialName is not empty
547
- if not mapping.material_name or mapping.material_name.strip() == '':
548
- print(f'Material mappings validation error: materialName cannot be empty at index {i}')
579
+ if not mapping.material_name or mapping.material_name.strip() == "":
580
+ print(
581
+ f"Material mappings validation error: materialName cannot be empty at index {i}"
582
+ )
549
583
  return False
550
584
 
551
585
  # Validate toolMaterialColor format
552
586
  if not hex_color_regex.match(mapping.tool_material_color):
553
- print(f'Material mappings validation error: toolMaterialColor must be in #RRGGBB format, got {mapping.tool_material_color} at index {i}')
587
+ print(
588
+ f"Material mappings validation error: toolMaterialColor must be in #RRGGBB format, got {mapping.tool_material_color} at index {i}"
589
+ )
554
590
  return False
555
591
 
556
592
  # Validate slotMaterialColor format
557
593
  if not hex_color_regex.match(mapping.slot_material_color):
558
- print(f'Material mappings validation error: slotMaterialColor must be in #RRGGBB format, got {mapping.slot_material_color} at index {i}')
594
+ print(
595
+ f"Material mappings validation error: slotMaterialColor must be in #RRGGBB format, got {mapping.slot_material_color} at index {i}"
596
+ )
559
597
  return False
560
598
 
561
599
  return True
@@ -1,7 +1,8 @@
1
1
  """
2
2
  FlashForge Python API - Temperature Control Module
3
3
  """
4
- from typing import TYPE_CHECKING, Optional
4
+
5
+ from typing import TYPE_CHECKING
5
6
 
6
7
  if TYPE_CHECKING:
7
8
  from ...client import FlashForgeClient
@@ -17,12 +18,12 @@ class TempControl:
17
18
  def __init__(self, client: "FlashForgeClient"):
18
19
  """
19
20
  Creates an instance of the TempControl class.
20
-
21
+
21
22
  Args:
22
23
  client: The FlashForgeClient instance used for communication with the printer.
23
24
  """
24
25
  self.client = client
25
- self._tcp_client: Optional[TcpClient] = None
26
+ self._tcp_client: TcpClient | None = None
26
27
 
27
28
  @property
28
29
  def tcp_client(self) -> "TcpClient":
@@ -34,11 +35,11 @@ class TempControl:
34
35
  async def set_extruder_temp(self, temperature: int, wait_for: bool = False) -> bool:
35
36
  """
36
37
  Sets the target temperature for an extruder.
37
-
38
+
38
39
  Args:
39
40
  temperature: The target temperature in Celsius.
40
41
  wait_for: Whether to wait for the heating operation to complete
41
-
42
+
42
43
  Returns:
43
44
  True if the command is successful, False otherwise.
44
45
  """
@@ -47,11 +48,11 @@ class TempControl:
47
48
  async def set_bed_temp(self, temperature: int, wait_for: bool = False) -> bool:
48
49
  """
49
50
  Sets the target temperature for the print bed.
50
-
51
+
51
52
  Args:
52
53
  temperature: The target bed temperature in Celsius.
53
54
  wait_for: Whether to wait for the heating operation to complete
54
-
55
+
55
56
  Returns:
56
57
  True if the command is successful, False otherwise.
57
58
  """
@@ -69,20 +70,22 @@ class TempControl:
69
70
  async def cancel_bed_temp(self) -> bool:
70
71
  """
71
72
  Cancels the heating of the print bed (sets target temperature to 0).
72
-
73
+
73
74
  Returns:
74
75
  True if the command is successful, False otherwise.
75
76
  """
76
77
  return await self.tcp_client.cancel_bed_temp()
77
78
 
78
- async def wait_for_part_cool(self, target_temp: float = 50.0, timeout_seconds: int = 1800) -> bool:
79
+ async def wait_for_part_cool(
80
+ self, target_temp: float = 50.0, timeout_seconds: int = 1800
81
+ ) -> bool:
79
82
  """
80
83
  Waits for printer components to cool down to a safe temperature.
81
-
84
+
82
85
  Args:
83
86
  target_temp: The target temperature to wait for (default: 50°C).
84
87
  timeout_seconds: Maximum time to wait in seconds (default: 30 minutes).
85
-
88
+
86
89
  Returns:
87
90
  True if components cooled to target temperature, False if timeout or error.
88
91
  """
@@ -2,7 +2,7 @@
2
2
  FlashForge Python API - Miscellaneous Utilities
3
3
  """
4
4
 
5
- from .temperature import Temperature
6
5
  from .scientific_notation import format_scientific_notation
6
+ from .temperature import Temperature
7
7
 
8
8
  __all__ = ["Temperature", "format_scientific_notation"]
@@ -1,8 +1,9 @@
1
1
  """
2
2
  FlashForge Python API - Network Package
3
3
  """
4
- from .utils import NetworkUtils
4
+
5
5
  from .fnet_code import FNetCode
6
+ from .utils import NetworkUtils
6
7
 
7
8
  __all__ = [
8
9
  "NetworkUtils",
@@ -1,7 +1,6 @@
1
1
  """
2
2
  FlashForge Python API - Network Utilities
3
3
  """
4
- from typing import Optional, Union
5
4
 
6
5
  from ...models.responses import GenericResponse
7
6
 
@@ -10,13 +9,13 @@ class NetworkUtils:
10
9
  """Utility class for handling network responses and status codes."""
11
10
 
12
11
  @staticmethod
13
- def is_ok(response: Optional[Union[GenericResponse, dict]]) -> bool:
12
+ def is_ok(response: GenericResponse | dict | None) -> bool:
14
13
  """
15
14
  Checks if a response indicates success.
16
-
15
+
17
16
  Args:
18
17
  response: The response object to check
19
-
18
+
20
19
  Returns:
21
20
  True if the response indicates success, False otherwise
22
21
  """
@@ -34,13 +33,13 @@ class NetworkUtils:
34
33
  return code in (0, 200)
35
34
 
36
35
  @staticmethod
37
- def get_error_message(response: Optional[Union[GenericResponse, dict]]) -> str:
36
+ def get_error_message(response: GenericResponse | dict | None) -> str:
38
37
  """
39
38
  Extracts error message from a response.
40
-
39
+
41
40
  Args:
42
41
  response: The response object to extract message from
43
-
42
+
44
43
  Returns:
45
44
  Error message string, or empty string if none found
46
45
  """
@@ -49,7 +48,7 @@ class NetworkUtils:
49
48
 
50
49
  # Handle dictionary responses
51
50
  if isinstance(response, dict):
52
- return response.get("message", "Unknown error")
51
+ return str(response.get("message", "Unknown error"))
53
52
  else:
54
53
  # Handle GenericResponse objects
55
54
  return getattr(response, "message", "Unknown error")