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,16 +1,17 @@
1
1
  """
2
2
  FlashForge Python API - Files Module
3
3
  """
4
+
4
5
  import base64
5
- from typing import TYPE_CHECKING, List, Optional
6
+ from typing import TYPE_CHECKING
6
7
 
7
8
  import aiohttp
9
+ from pydantic import ValidationError
8
10
 
9
11
  from ...models.machine_info import FFGcodeFileEntry
10
- from ...models.responses import GenericResponse, GCodeListResponse, ThumbnailResponse
12
+ from ...models.responses import GCodeListResponse, ThumbnailResponse
11
13
  from ..constants.endpoints import Endpoints
12
14
  from ..network.utils import NetworkUtils
13
- from pydantic import ValidationError
14
15
 
15
16
  if TYPE_CHECKING:
16
17
  from ...client import FlashForgeClient
@@ -25,34 +26,34 @@ class Files:
25
26
  def __init__(self, client: "FlashForgeClient"):
26
27
  """
27
28
  Creates an instance of the Files class.
28
-
29
+
29
30
  Args:
30
31
  client: The FlashForgeClient instance used for communication with the printer.
31
32
  """
32
33
  self.client = client
33
34
 
34
- async def get_file_list(self) -> List[str]:
35
+ async def get_file_list(self) -> list[str]:
35
36
  """
36
37
  Retrieves a list of files stored locally on the printer.
37
-
38
+
38
39
  Returns:
39
40
  A list of file names, or empty list if retrieval fails.
40
41
  """
41
42
  # This method uses the TCP client to get file list
42
- if hasattr(self.client, 'tcp_client') and self.client.tcp_client:
43
+ if hasattr(self.client, "tcp_client") and self.client.tcp_client:
43
44
  return await self.client.tcp_client.get_file_list_async()
44
45
  return []
45
46
 
46
- async def get_local_file_list(self) -> List[str]:
47
+ async def get_local_file_list(self) -> list[str]:
47
48
  """
48
49
  Retrieves a list of files stored locally on the printer.
49
-
50
+
50
51
  Returns:
51
52
  A list of file names, or empty list if retrieval fails.
52
53
  """
53
54
  return await self.get_file_list()
54
55
 
55
- async def get_recent_file_list(self) -> List[FFGcodeFileEntry]:
56
+ async def get_recent_file_list(self) -> list[FFGcodeFileEntry]:
56
57
  """
57
58
  Retrieves a list of the 10 most recently printed files from the printer's API.
58
59
  For AD5X and newer printers, returns detailed file entries with material info.
@@ -61,17 +62,14 @@ class Files:
61
62
  Returns:
62
63
  A list of FFGcodeFileEntry objects. Returns an empty list if the request fails or an error occurs.
63
64
  """
64
- payload = {
65
- "serialNumber": self.client.serial_number,
66
- "checkCode": self.client.check_code
67
- }
65
+ payload = {"serialNumber": self.client.serial_number, "checkCode": self.client.check_code}
68
66
 
69
67
  try:
70
68
  async with aiohttp.ClientSession() as session:
71
69
  async with session.post(
72
70
  self.client.get_endpoint(Endpoints.GCODE_LIST),
73
71
  json=payload,
74
- headers={"Content-Type": "application/json"}
72
+ headers={"Content-Type": "application/json"},
75
73
  ) as response:
76
74
  if response.status != 200:
77
75
  return []
@@ -84,6 +82,7 @@ class Files:
84
82
  # Fallback: manually parse as JSON if Content-Type is malformed
85
83
  text = await response.text()
86
84
  import json
85
+
87
86
  data = json.loads(text)
88
87
 
89
88
  if not NetworkUtils.is_ok(data):
@@ -96,7 +95,7 @@ class Files:
96
95
  except ValidationError:
97
96
  raw_list = data.get("gcodeList", [])
98
97
  if isinstance(raw_list, list):
99
- entries: List[FFGcodeFileEntry] = []
98
+ entries: list[FFGcodeFileEntry] = []
100
99
  for file_name in raw_list:
101
100
  if isinstance(file_name, str):
102
101
  entries.append(
@@ -120,15 +119,17 @@ class Files:
120
119
  if isinstance(first_item, str):
121
120
  # Convert string array to FFGcodeFileEntry objects
122
121
  return [
123
- FFGcodeFileEntry(
124
- gcodeFileName=file_name,
125
- printingTime=0
126
- )
122
+ FFGcodeFileEntry(gcodeFileName=file_name, printingTime=0)
127
123
  for file_name in result.gcode_list
124
+ if isinstance(file_name, str)
125
+ ]
126
+ elif isinstance(first_item, FFGcodeFileEntry):
127
+ # Already FFGcodeFileEntry objects - need explicit type narrowing
128
+ return [
129
+ item
130
+ for item in result.gcode_list
131
+ if isinstance(item, FFGcodeFileEntry)
128
132
  ]
129
- else:
130
- # Already FFGcodeFileEntry objects
131
- return result.gcode_list
132
133
 
133
134
  return []
134
135
 
@@ -136,7 +137,7 @@ class Files:
136
137
  print(f"GetRecentFileList error: {err}")
137
138
  return []
138
139
 
139
- async def get_gcode_thumbnail(self, file_name: str) -> Optional[bytes]:
140
+ async def get_gcode_thumbnail(self, file_name: str) -> bytes | None:
140
141
  """
141
142
  Retrieves the thumbnail image for a specified G-code file.
142
143
  The image data is returned as bytes.
@@ -151,7 +152,7 @@ class Files:
151
152
  payload = {
152
153
  "serialNumber": self.client.serial_number,
153
154
  "checkCode": self.client.check_code,
154
- "fileName": file_name
155
+ "fileName": file_name,
155
156
  }
156
157
 
157
158
  try:
@@ -159,7 +160,7 @@ class Files:
159
160
  async with session.post(
160
161
  self.client.get_endpoint(Endpoints.GCODE_THUMB),
161
162
  json=payload,
162
- headers={"Content-Type": "application/json"}
163
+ headers={"Content-Type": "application/json"},
163
164
  ) as response:
164
165
  if response.status != 200:
165
166
  return None
@@ -172,6 +173,7 @@ class Files:
172
173
  # Fallback: manually parse as JSON if Content-Type is malformed
173
174
  text = await response.text()
174
175
  import json
176
+
175
177
  data = json.loads(text)
176
178
 
177
179
  if NetworkUtils.is_ok(data):
@@ -1,12 +1,14 @@
1
1
  """
2
2
  FlashForge Python API - Info Module
3
3
  """
4
- from typing import TYPE_CHECKING, Optional
4
+
5
+ from datetime import datetime, timedelta
6
+ from typing import TYPE_CHECKING
5
7
 
6
8
  import aiohttp
7
9
 
8
- from ...models.machine_info import FFMachineInfo, MachineState
9
- from ...models.responses import DetailResponse
10
+ from ...models.machine_info import FFMachineInfo, MachineState, Temperature
11
+ from ...models.responses import DetailResponse, FFPrinterDetail
10
12
  from ..constants.endpoints import Endpoints
11
13
 
12
14
  if TYPE_CHECKING:
@@ -21,13 +23,13 @@ class MachineInfoParser:
21
23
  """
22
24
 
23
25
  @staticmethod
24
- def from_detail(detail) -> Optional[FFMachineInfo]:
26
+ def from_detail(detail: FFPrinterDetail | None) -> FFMachineInfo | None:
25
27
  """
26
28
  Converts printer details from the API response format to our internal FFMachineInfo model.
27
-
29
+
28
30
  Args:
29
31
  detail: The FFPrinterDetail object received from the printer's API.
30
-
32
+
31
33
  Returns:
32
34
  An FFMachineInfo object containing structured and formatted printer information,
33
35
  or None if the input detail is None or an error occurs during processing.
@@ -36,42 +38,13 @@ class MachineInfoParser:
36
38
  return None
37
39
 
38
40
  try:
39
- # Helper function to format time from seconds
40
- def format_time_from_seconds(seconds: float) -> str:
41
- try:
42
- valid_seconds = int(seconds) if isinstance(seconds, (int, float)) else 0
43
- hours = valid_seconds // 3600
44
- minutes = (valid_seconds % 3600) // 60
45
- return f"{hours:02d}:{minutes:02d}"
46
- except Exception:
47
- return "00:00"
48
-
49
- # Helper function to get machine state
50
- def get_machine_state(status: str) -> MachineState:
51
- valid_status = status.lower() if isinstance(status, str) else ""
52
- state_mapping = {
53
- "ready": MachineState.READY,
54
- "busy": MachineState.BUSY,
55
- "calibrate_doing": MachineState.CALIBRATING,
56
- "error": MachineState.ERROR,
57
- "heating": MachineState.HEATING,
58
- "printing": MachineState.PRINTING,
59
- "pausing": MachineState.PAUSING,
60
- "paused": MachineState.PAUSED,
61
- "cancel": MachineState.CANCELLED,
62
- "completed": MachineState.COMPLETED,
63
- }
64
-
65
- if valid_status in state_mapping:
66
- return state_mapping[valid_status]
67
-
68
- if valid_status:
69
- print(f"Unknown machine status received: '{status}'")
70
- return MachineState.UNKNOWN
71
-
72
41
  # Calculate derived values
73
- print_eta = format_time_from_seconds(getattr(detail, "estimated_time", 0) or 0)
74
- formatted_run_time = format_time_from_seconds(getattr(detail, "print_duration", 0) or 0)
42
+ estimated_time = getattr(detail, "estimated_time", 0) or 0
43
+ print_eta = MachineInfoParser._format_time_from_seconds(estimated_time)
44
+ formatted_run_time = MachineInfoParser._format_time_from_seconds(
45
+ getattr(detail, "print_duration", 0) or 0
46
+ )
47
+ completion_time = datetime.now() + timedelta(seconds=estimated_time)
75
48
 
76
49
  total_minutes = getattr(detail, "cumulative_print_time", 0) or 0
77
50
  hours = total_minutes // 60
@@ -90,67 +63,63 @@ class MachineInfoParser:
90
63
  print_progress = getattr(detail, "print_progress", 0) or 0
91
64
  est_length = total_job_filament_meters * print_progress
92
65
  est_weight = (getattr(detail, "estimated_right_weight", 0) or 0) * print_progress
66
+ has_material_station = (
67
+ getattr(detail, "has_matl_station", None) is True
68
+ or (getattr(getattr(detail, "matl_station_info", None), "slot_cnt", 0) or 0) > 0
69
+ or len(getattr(getattr(detail, "matl_station_info", None), "slot_infos", []) or []) > 0
70
+ )
71
+ printer_name = getattr(detail, "name", "") or ""
72
+ is_ad5x = printer_name.upper() == "AD5X" or has_material_station
73
+ is_pro = "Pro" in printer_name and not is_ad5x
93
74
 
94
75
  # Build the FFMachineInfo object
95
76
  machine_info = FFMachineInfo(
96
77
  # Auto-shutdown settings
97
78
  auto_shutdown=auto_shutdown,
98
79
  auto_shutdown_time=getattr(detail, "auto_shutdown_time", 0) or 0,
99
-
100
80
  # Camera
101
81
  camera_stream_url=getattr(detail, "camera_stream_url", "") or "",
102
-
103
82
  # Fan speeds
104
83
  chamber_fan_speed=getattr(detail, "chamber_fan_speed", 0) or 0,
105
84
  cooling_fan_speed=getattr(detail, "cooling_fan_speed", 0) or 0,
106
85
  cooling_fan_left_speed=getattr(detail, "cooling_fan_left_speed", None),
107
-
108
86
  # Cumulative stats
109
87
  cumulative_filament=getattr(detail, "cumulative_filament", 0) or 0,
110
88
  cumulative_print_time=getattr(detail, "cumulative_print_time", 0) or 0,
111
-
112
89
  # Current print speed
113
90
  current_print_speed=getattr(detail, "current_print_speed", 0) or 0,
114
-
115
91
  # Disk space
116
92
  free_disk_space=f"{(getattr(detail, 'remaining_disk_space', 0) or 0):.2f}",
117
-
118
93
  # Door and error status
119
94
  door_open=door_open,
120
95
  error_code=getattr(detail, "error_code", "") or "",
121
-
122
96
  # Current print estimates
123
97
  est_length=est_length,
124
98
  est_weight=est_weight,
125
99
  estimated_time=getattr(detail, "estimated_time", 0) or 0,
126
-
127
100
  # Fans & LED status
128
101
  external_fan_on=external_fan_on,
129
102
  internal_fan_on=internal_fan_on,
130
103
  lights_on=lights_on,
131
-
132
104
  # Network
133
105
  ip_address=getattr(detail, "ip_addr", "") or "",
134
106
  mac_address=getattr(detail, "mac_addr", "") or "",
135
-
136
107
  # Print settings
137
108
  fill_amount=getattr(detail, "fill_amount", 0) or 0,
138
109
  firmware_version=getattr(detail, "firmware_version", "") or "",
139
- name=getattr(detail, "name", "") or "",
140
- is_pro="Pro" in (getattr(detail, "name", "") or ""),
141
- is_ad5x="AD5X" in (getattr(detail, "name", "") or "").upper(),
110
+ name=printer_name,
111
+ is_pro=is_pro,
112
+ is_ad5x=is_ad5x,
142
113
  nozzle_size=getattr(detail, "nozzle_model", "") or "",
143
-
144
114
  # Temperatures
145
- print_bed={
146
- "current": getattr(detail, "plat_temp", 0) or 0,
147
- "set": getattr(detail, "plat_target_temp", 0) or 0
148
- },
149
- extruder={
150
- "current": getattr(detail, "right_temp", 0) or 0,
151
- "set": getattr(detail, "right_target_temp", 0) or 0
152
- },
153
-
115
+ print_bed=Temperature(
116
+ current=getattr(detail, "plat_temp", 0) or 0,
117
+ set=getattr(detail, "plat_target_temp", 0) or 0,
118
+ ),
119
+ extruder=Temperature(
120
+ current=getattr(detail, "right_temp", 0) or 0,
121
+ set=getattr(detail, "right_target_temp", 0) or 0,
122
+ ),
154
123
  # Current print stats
155
124
  print_duration=getattr(detail, "print_duration", 0) or 0,
156
125
  print_file_name=getattr(detail, "print_file_name", "") or "",
@@ -160,23 +129,20 @@ class MachineInfoParser:
160
129
  print_progress_int=int(print_progress * 100),
161
130
  print_speed_adjust=getattr(detail, "print_speed_adjust", 0) or 0,
162
131
  filament_type=getattr(detail, "right_filament_type", "") or "",
163
-
164
132
  # Machine state
165
- machine_state=get_machine_state(getattr(detail, "status", "") or ""),
133
+ machine_state=MachineInfoParser._get_machine_state(getattr(detail, "status", "") or ""),
166
134
  status=getattr(detail, "status", "") or "",
167
135
  total_print_layers=getattr(detail, "target_print_layer", 0) or 0,
168
136
  tvoc=getattr(detail, "tvoc", 0) or 0,
169
137
  z_axis_compensation=getattr(detail, "z_axis_compensation", 0) or 0,
170
-
171
138
  # Cloud codes
172
139
  flash_cloud_register_code=getattr(detail, "flash_register_code", "") or "",
173
140
  polar_cloud_register_code=getattr(detail, "polar_register_code", "") or "",
174
-
175
141
  # Extras
176
142
  print_eta=print_eta,
143
+ completion_time=completion_time,
177
144
  formatted_run_time=formatted_run_time,
178
145
  formatted_total_run_time=formatted_total_run_time,
179
-
180
146
  # AD5X Material Station
181
147
  has_matl_station=getattr(detail, "has_matl_station", None),
182
148
  matl_station_info=getattr(detail, "matl_station_info", None),
@@ -190,6 +156,41 @@ class MachineInfoParser:
190
156
  print(f"Detail object causing error: {detail}")
191
157
  return None
192
158
 
159
+ @staticmethod
160
+ def _format_time_from_seconds(seconds: float) -> str:
161
+ """Format a duration in seconds as HH:MM."""
162
+ try:
163
+ valid_seconds = int(seconds) if isinstance(seconds, (int, float)) else 0
164
+ hours = valid_seconds // 3600
165
+ minutes = (valid_seconds % 3600) // 60
166
+ return f"{hours:02d}:{minutes:02d}"
167
+ except Exception:
168
+ return "00:00"
169
+
170
+ @staticmethod
171
+ def _get_machine_state(status: str) -> MachineState:
172
+ """Map raw status strings into the public machine state enum."""
173
+ valid_status = status.lower() if isinstance(status, str) else ""
174
+ state_mapping = {
175
+ "ready": MachineState.READY,
176
+ "busy": MachineState.BUSY,
177
+ "calibrate_doing": MachineState.CALIBRATING,
178
+ "error": MachineState.ERROR,
179
+ "heating": MachineState.HEATING,
180
+ "printing": MachineState.PRINTING,
181
+ "pausing": MachineState.PAUSING,
182
+ "paused": MachineState.PAUSED,
183
+ "cancel": MachineState.CANCELLED,
184
+ "completed": MachineState.COMPLETED,
185
+ }
186
+
187
+ if valid_status in state_mapping:
188
+ return state_mapping[valid_status]
189
+
190
+ if valid_status:
191
+ print(f"Unknown machine status received: '{status}'")
192
+ return MachineState.UNKNOWN
193
+
193
194
 
194
195
  class Info:
195
196
  """
@@ -200,17 +201,17 @@ class Info:
200
201
  def __init__(self, client: "FlashForgeClient"):
201
202
  """
202
203
  Creates an instance of the Info class.
203
-
204
+
204
205
  Args:
205
206
  client: The FlashForgeClient instance used for communication with the printer.
206
207
  """
207
208
  self.client = client
208
209
 
209
- async def get(self) -> Optional[FFMachineInfo]:
210
+ async def get(self) -> FFMachineInfo | None:
210
211
  """
211
212
  Retrieves comprehensive machine information, processed into the FFMachineInfo model.
212
213
  This method fetches detailed data from the printer and transforms it.
213
-
214
+
214
215
  Returns:
215
216
  An FFMachineInfo object, or None if an error occurs or no data is returned.
216
217
  """
@@ -222,53 +223,50 @@ class Info:
222
223
  async def is_printing(self) -> bool:
223
224
  """
224
225
  Checks if the printer is currently in the "printing" state.
225
-
226
+
226
227
  Returns:
227
228
  True if the printer is printing, False otherwise or if status cannot be determined.
228
229
  """
229
230
  info = await self.get()
230
231
  return info.status == "printing" if info else False
231
232
 
232
- async def get_status(self) -> Optional[str]:
233
+ async def get_status(self) -> str | None:
233
234
  """
234
235
  Retrieves the raw status string of the printer (e.g., "ready", "printing", "error").
235
-
236
+
236
237
  Returns:
237
238
  The status string, or None if it cannot be determined.
238
239
  """
239
240
  info = await self.get()
240
241
  return info.status if info else None
241
242
 
242
- async def get_machine_state(self) -> Optional[MachineState]:
243
+ async def get_machine_state(self) -> MachineState | None:
243
244
  """
244
245
  Retrieves the machine state as a MachineState enum value.
245
-
246
+
246
247
  Returns:
247
248
  A MachineState enum value, or None if it cannot be determined.
248
249
  """
249
250
  info = await self.get()
250
251
  return info.machine_state if info else None
251
252
 
252
- async def get_detail_response(self) -> Optional[DetailResponse]:
253
+ async def get_detail_response(self) -> DetailResponse | None:
253
254
  """
254
255
  Retrieves the raw detailed response from the printer's detail endpoint.
255
256
  This contains a wealth of information about the printer's current state.
256
-
257
+
257
258
  Returns:
258
259
  A DetailResponse object containing the raw printer details,
259
260
  or None if the request fails or an error occurs.
260
261
  """
261
- payload = {
262
- "serialNumber": self.client.serial_number,
263
- "checkCode": self.client.check_code
264
- }
262
+ payload = {"serialNumber": self.client.serial_number, "checkCode": self.client.check_code}
265
263
 
266
264
  try:
267
265
  async with aiohttp.ClientSession() as session:
268
266
  async with session.post(
269
267
  self.client.get_endpoint(Endpoints.DETAIL),
270
268
  json=payload,
271
- headers={"Content-Type": "application/json"}
269
+ headers={"Content-Type": "application/json"},
272
270
  ) as response:
273
271
  if response.status != 200:
274
272
  print(f"Non-200 status from detail endpoint: {response.status}")
@@ -283,8 +281,9 @@ class Info:
283
281
  # Fallback: manually parse as JSON if Content-Type is malformed
284
282
  text = await response.text()
285
283
  import json
284
+
286
285
  data = json.loads(text)
287
-
286
+
288
287
  return DetailResponse(**data)
289
288
 
290
289
  except Exception as error: