flashforge-python-api 1.0.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 +188 -0
  2. flashforge/api/__init__.py +17 -0
  3. flashforge/api/constants/__init__.py +10 -0
  4. flashforge/api/constants/commands.py +10 -0
  5. flashforge/api/constants/endpoints.py +11 -0
  6. flashforge/api/controls/__init__.py +16 -0
  7. flashforge/api/controls/control.py +357 -0
  8. flashforge/api/controls/files.py +171 -0
  9. flashforge/api/controls/info.py +292 -0
  10. flashforge/api/controls/job_control.py +561 -0
  11. flashforge/api/controls/temp_control.py +89 -0
  12. flashforge/api/filament/__init__.py +7 -0
  13. flashforge/api/filament/filament.py +39 -0
  14. flashforge/api/misc/__init__.py +8 -0
  15. flashforge/api/misc/scientific_notation.py +28 -0
  16. flashforge/api/misc/temperature.py +42 -0
  17. flashforge/api/network/__init__.py +10 -0
  18. flashforge/api/network/fnet_code.py +15 -0
  19. flashforge/api/network/utils.py +55 -0
  20. flashforge/client.py +381 -0
  21. flashforge/discovery/__init__.py +12 -0
  22. flashforge/discovery/discovery.py +388 -0
  23. flashforge/models/__init__.py +50 -0
  24. flashforge/models/machine_info.py +247 -0
  25. flashforge/models/responses.py +123 -0
  26. flashforge/tcp/__init__.py +41 -0
  27. flashforge/tcp/ff_client.py +587 -0
  28. flashforge/tcp/gcode/__init__.py +11 -0
  29. flashforge/tcp/gcode/gcode_controller.py +296 -0
  30. flashforge/tcp/gcode/gcodes.py +93 -0
  31. flashforge/tcp/parsers/__init__.py +27 -0
  32. flashforge/tcp/parsers/endstop_status.py +273 -0
  33. flashforge/tcp/parsers/location_info.py +68 -0
  34. flashforge/tcp/parsers/print_status.py +182 -0
  35. flashforge/tcp/parsers/printer_info.py +154 -0
  36. flashforge/tcp/parsers/temp_info.py +217 -0
  37. flashforge/tcp/parsers/thumbnail_info.py +242 -0
  38. flashforge/tcp/tcp_client.py +448 -0
  39. flashforge_python_api-1.0.0.dist-info/METADATA +123 -0
  40. flashforge_python_api-1.0.0.dist-info/RECORD +43 -0
  41. flashforge_python_api-1.0.0.dist-info/WHEEL +4 -0
  42. flashforge_python_api-1.0.0.dist-info/entry_points.txt +2 -0
  43. flashforge_python_api-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,171 @@
1
+ """
2
+ FlashForge Python API - Files Module
3
+ """
4
+ import base64
5
+ from typing import TYPE_CHECKING, List, Optional
6
+
7
+ import aiohttp
8
+
9
+ from ...models.machine_info import FFGcodeFileEntry
10
+ from ...models.responses import GenericResponse, GCodeListResponse, ThumbnailResponse
11
+ from ..constants.endpoints import Endpoints
12
+ from ..network.utils import NetworkUtils
13
+
14
+ if TYPE_CHECKING:
15
+ from ...client import FlashForgeClient
16
+
17
+
18
+ class Files:
19
+ """
20
+ Provides methods for managing files on the FlashForge 3D printer.
21
+ This includes retrieving file lists and thumbnails.
22
+ """
23
+
24
+ def __init__(self, client: "FlashForgeClient"):
25
+ """
26
+ Creates an instance of the Files class.
27
+
28
+ Args:
29
+ client: The FlashForgeClient instance used for communication with the printer.
30
+ """
31
+ self.client = client
32
+
33
+ async def get_file_list(self) -> List[str]:
34
+ """
35
+ Retrieves a list of files stored locally on the printer.
36
+
37
+ Returns:
38
+ A list of file names, or empty list if retrieval fails.
39
+ """
40
+ # This method uses the TCP client to get file list
41
+ if hasattr(self.client, 'tcp_client') and self.client.tcp_client:
42
+ return await self.client.tcp_client.get_file_list_async()
43
+ return []
44
+
45
+ async def get_local_file_list(self) -> List[str]:
46
+ """
47
+ Retrieves a list of files stored locally on the printer.
48
+
49
+ Returns:
50
+ A list of file names, or empty list if retrieval fails.
51
+ """
52
+ return await self.get_file_list()
53
+
54
+ async def get_recent_file_list(self) -> List[FFGcodeFileEntry]:
55
+ """
56
+ Retrieves a list of the 10 most recently printed files from the printer's API.
57
+ For AD5X and newer printers, returns detailed file entries with material info.
58
+ For older printers, returns basic file entries with normalized data.
59
+
60
+ Returns:
61
+ A list of FFGcodeFileEntry objects. Returns an empty list if the request fails or an error occurs.
62
+ """
63
+ payload = {
64
+ "serialNumber": self.client.serial_number,
65
+ "checkCode": self.client.check_code
66
+ }
67
+
68
+ try:
69
+ async with aiohttp.ClientSession() as session:
70
+ async with session.post(
71
+ self.client.get_endpoint(Endpoints.GCODE_LIST),
72
+ json=payload,
73
+ headers={"Content-Type": "application/json"}
74
+ ) as response:
75
+ if response.status != 200:
76
+ return []
77
+
78
+ # Fix for FlashForge printer's malformed Content-Type header
79
+ # Some printers return "appliation/json" instead of "application/json"
80
+ try:
81
+ data = await response.json()
82
+ except aiohttp.ContentTypeError:
83
+ # Fallback: manually parse as JSON if Content-Type is malformed
84
+ text = await response.text()
85
+ import json
86
+ data = json.loads(text)
87
+
88
+ if not NetworkUtils.is_ok(data):
89
+ print(f"Error retrieving file list: {NetworkUtils.get_error_message(data)}")
90
+ return []
91
+
92
+ # Parse the response using GCodeListResponse
93
+ result = GCodeListResponse(**data)
94
+
95
+ # AD5X and newer printers provide detailed info in gcodeListDetail
96
+ if result.gcode_list_detail and len(result.gcode_list_detail) > 0:
97
+ return result.gcode_list_detail
98
+
99
+ # Fallback for older printers using gcodeList
100
+ if result.gcode_list and len(result.gcode_list) > 0:
101
+ # Check if it's a list of strings or already FFGcodeFileEntry objects
102
+ first_item = result.gcode_list[0]
103
+
104
+ if isinstance(first_item, str):
105
+ # Convert string array to FFGcodeFileEntry objects
106
+ return [
107
+ FFGcodeFileEntry(
108
+ gcode_file_name=file_name,
109
+ printing_time=0
110
+ )
111
+ for file_name in result.gcode_list
112
+ ]
113
+ else:
114
+ # Already FFGcodeFileEntry objects
115
+ return result.gcode_list
116
+
117
+ return []
118
+
119
+ except Exception as err:
120
+ print(f"GetRecentFileList error: {err}")
121
+ return []
122
+
123
+ async def get_gcode_thumbnail(self, file_name: str) -> Optional[bytes]:
124
+ """
125
+ Retrieves the thumbnail image for a specified G-code file.
126
+ The image data is returned as bytes.
127
+
128
+ Args:
129
+ file_name: The name of the G-code file (e.g., "my_print.gcode") for which to retrieve the thumbnail.
130
+
131
+ Returns:
132
+ Bytes containing the thumbnail image data (decoded from base64),
133
+ or None if the request fails, the file has no thumbnail, or an error occurs.
134
+ """
135
+ payload = {
136
+ "serialNumber": self.client.serial_number,
137
+ "checkCode": self.client.check_code,
138
+ "fileName": file_name
139
+ }
140
+
141
+ try:
142
+ async with aiohttp.ClientSession() as session:
143
+ async with session.post(
144
+ self.client.get_endpoint(Endpoints.GCODE_THUMB),
145
+ json=payload,
146
+ headers={"Content-Type": "application/json"}
147
+ ) as response:
148
+ if response.status != 200:
149
+ return None
150
+
151
+ # Fix for FlashForge printer's malformed Content-Type header
152
+ # Some printers return "appliation/json" instead of "application/json"
153
+ try:
154
+ data = await response.json()
155
+ except aiohttp.ContentTypeError:
156
+ # Fallback: manually parse as JSON if Content-Type is malformed
157
+ text = await response.text()
158
+ import json
159
+ data = json.loads(text)
160
+
161
+ if NetworkUtils.is_ok(data):
162
+ # Parse response and return decoded image bytes
163
+ result = ThumbnailResponse(**data)
164
+ return base64.b64decode(result.image_data)
165
+ else:
166
+ print(f"Error retrieving thumbnail: {NetworkUtils.get_error_message(data)}")
167
+ return None
168
+
169
+ except Exception as err:
170
+ print(f"GetGcodeThumbnail error: {err}")
171
+ return None
@@ -0,0 +1,292 @@
1
+ """
2
+ FlashForge Python API - Info Module
3
+ """
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ import aiohttp
7
+
8
+ from ...models.machine_info import FFMachineInfo, MachineState
9
+ from ...models.responses import DetailResponse
10
+ from ..constants.endpoints import Endpoints
11
+
12
+ if TYPE_CHECKING:
13
+ from ...client import FlashForgeClient
14
+
15
+
16
+ class MachineInfoParser:
17
+ """
18
+ Transforms printer detail data from the API response format into a structured FFMachineInfo object.
19
+ This class centralizes the logic for mapping and calculating various properties of the printer's state
20
+ and capabilities based on the raw data received from the printer.
21
+ """
22
+
23
+ @staticmethod
24
+ def from_detail(detail) -> Optional[FFMachineInfo]:
25
+ """
26
+ Converts printer details from the API response format to our internal FFMachineInfo model.
27
+
28
+ Args:
29
+ detail: The FFPrinterDetail object received from the printer's API.
30
+
31
+ Returns:
32
+ An FFMachineInfo object containing structured and formatted printer information,
33
+ or None if the input detail is None or an error occurs during processing.
34
+ """
35
+ if not detail:
36
+ return None
37
+
38
+ try:
39
+ # Helper function to format time from seconds
40
+ def format_time_from_seconds(seconds: int) -> str:
41
+ try:
42
+ valid_seconds = seconds if isinstance(seconds, int) 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
+ # 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)
75
+
76
+ total_minutes = getattr(detail, "cumulative_print_time", 0) or 0
77
+ hours = total_minutes // 60
78
+ minutes = total_minutes % 60
79
+ formatted_total_run_time = f"{hours}h:{minutes}m"
80
+
81
+ # Boolean status conversions
82
+ auto_shutdown = (getattr(detail, "auto_shutdown", "") or "") == "open"
83
+ door_open = (getattr(detail, "door_status", "") or "") == "open"
84
+ external_fan_on = (getattr(detail, "external_fan_status", "") or "") == "open"
85
+ internal_fan_on = (getattr(detail, "internal_fan_status", "") or "") == "open"
86
+ lights_on = (getattr(detail, "light_status", "") or "") == "open"
87
+
88
+ # Calculate filament estimates
89
+ total_job_filament_meters = (getattr(detail, "estimated_right_len", 0) or 0) / 1000.0
90
+ print_progress = getattr(detail, "print_progress", 0) or 0
91
+ est_length = total_job_filament_meters * print_progress
92
+ est_weight = (getattr(detail, "estimated_right_weight", 0) or 0) * print_progress
93
+
94
+ # Build the FFMachineInfo object
95
+ machine_info = FFMachineInfo(
96
+ # Auto-shutdown settings
97
+ auto_shutdown=auto_shutdown,
98
+ auto_shutdown_time=getattr(detail, "auto_shutdown_time", 0) or 0,
99
+
100
+ # Camera
101
+ camera_stream_url=getattr(detail, "camera_stream_url", "") or "",
102
+
103
+ # Fan speeds
104
+ chamber_fan_speed=getattr(detail, "chamber_fan_speed", 0) or 0,
105
+ cooling_fan_speed=getattr(detail, "cooling_fan_speed", 0) or 0,
106
+ cooling_fan_left_speed=getattr(detail, "cooling_fan_left_speed", None),
107
+
108
+ # Cumulative stats
109
+ cumulative_filament=getattr(detail, "cumulative_filament", 0) or 0,
110
+ cumulative_print_time=getattr(detail, "cumulative_print_time", 0) or 0,
111
+
112
+ # Current print speed
113
+ current_print_speed=getattr(detail, "current_print_speed", 0) or 0,
114
+
115
+ # Disk space
116
+ free_disk_space=f"{(getattr(detail, 'remaining_disk_space', 0) or 0):.2f}",
117
+
118
+ # Door and error status
119
+ door_open=door_open,
120
+ error_code=getattr(detail, "error_code", "") or "",
121
+
122
+ # Current print estimates
123
+ est_length=est_length,
124
+ est_weight=est_weight,
125
+ estimated_time=getattr(detail, "estimated_time", 0) or 0,
126
+
127
+ # Fans & LED status
128
+ external_fan_on=external_fan_on,
129
+ internal_fan_on=internal_fan_on,
130
+ lights_on=lights_on,
131
+
132
+ # Network
133
+ ip_address=getattr(detail, "ip_addr", "") or "",
134
+ mac_address=getattr(detail, "mac_addr", "") or "",
135
+
136
+ # Print settings
137
+ fill_amount=getattr(detail, "fill_amount", 0) or 0,
138
+ 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(),
142
+ nozzle_size=getattr(detail, "nozzle_model", "") or "",
143
+
144
+ # 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
+
154
+ # Current print stats
155
+ print_duration=getattr(detail, "print_duration", 0) or 0,
156
+ print_file_name=getattr(detail, "print_file_name", "") or "",
157
+ print_file_thumb_url=getattr(detail, "print_file_thumb_url", "") or "",
158
+ current_print_layer=getattr(detail, "print_layer", 0) or 0,
159
+ print_progress=print_progress,
160
+ print_progress_int=int(print_progress * 100),
161
+ print_speed_adjust=getattr(detail, "print_speed_adjust", 0) or 0,
162
+ filament_type=getattr(detail, "right_filament_type", "") or "",
163
+
164
+ # Machine state
165
+ machine_state=get_machine_state(getattr(detail, "status", "") or ""),
166
+ status=getattr(detail, "status", "") or "",
167
+ total_print_layers=getattr(detail, "target_print_layer", 0) or 0,
168
+ tvoc=getattr(detail, "tvoc", 0) or 0,
169
+ z_axis_compensation=getattr(detail, "z_axis_compensation", 0) or 0,
170
+
171
+ # Cloud codes
172
+ flash_cloud_register_code=getattr(detail, "flash_register_code", "") or "",
173
+ polar_cloud_register_code=getattr(detail, "polar_register_code", "") or "",
174
+
175
+ # Extras
176
+ print_eta=print_eta,
177
+ formatted_run_time=formatted_run_time,
178
+ formatted_total_run_time=formatted_total_run_time,
179
+
180
+ # AD5X Material Station
181
+ has_matl_station=getattr(detail, "has_matl_station", None),
182
+ matl_station_info=getattr(detail, "matl_station_info", None),
183
+ indep_matl_info=getattr(detail, "indep_matl_info", None),
184
+ )
185
+
186
+ return machine_info
187
+
188
+ except Exception as error:
189
+ print(f"Error in MachineInfoParser.from_detail: {error}")
190
+ print(f"Detail object causing error: {detail}")
191
+ return None
192
+
193
+
194
+ class Info:
195
+ """
196
+ Provides methods for retrieving various information and status details from the FlashForge 3D printer.
197
+ This includes general machine information, printing status, and raw detail responses.
198
+ """
199
+
200
+ def __init__(self, client: "FlashForgeClient"):
201
+ """
202
+ Creates an instance of the Info class.
203
+
204
+ Args:
205
+ client: The FlashForgeClient instance used for communication with the printer.
206
+ """
207
+ self.client = client
208
+
209
+ async def get(self) -> Optional[FFMachineInfo]:
210
+ """
211
+ Retrieves comprehensive machine information, processed into the FFMachineInfo model.
212
+ This method fetches detailed data from the printer and transforms it.
213
+
214
+ Returns:
215
+ An FFMachineInfo object, or None if an error occurs or no data is returned.
216
+ """
217
+ detail_response = await self.get_detail_response()
218
+ if detail_response and detail_response.detail:
219
+ return MachineInfoParser.from_detail(detail_response.detail)
220
+ return None
221
+
222
+ async def is_printing(self) -> bool:
223
+ """
224
+ Checks if the printer is currently in the "printing" state.
225
+
226
+ Returns:
227
+ True if the printer is printing, False otherwise or if status cannot be determined.
228
+ """
229
+ info = await self.get()
230
+ return info.status == "printing" if info else False
231
+
232
+ async def get_status(self) -> Optional[str]:
233
+ """
234
+ Retrieves the raw status string of the printer (e.g., "ready", "printing", "error").
235
+
236
+ Returns:
237
+ The status string, or None if it cannot be determined.
238
+ """
239
+ info = await self.get()
240
+ return info.status if info else None
241
+
242
+ async def get_machine_state(self) -> Optional[MachineState]:
243
+ """
244
+ Retrieves the machine state as a MachineState enum value.
245
+
246
+ Returns:
247
+ A MachineState enum value, or None if it cannot be determined.
248
+ """
249
+ info = await self.get()
250
+ return info.machine_state if info else None
251
+
252
+ async def get_detail_response(self) -> Optional[DetailResponse]:
253
+ """
254
+ Retrieves the raw detailed response from the printer's detail endpoint.
255
+ This contains a wealth of information about the printer's current state.
256
+
257
+ Returns:
258
+ A DetailResponse object containing the raw printer details,
259
+ or None if the request fails or an error occurs.
260
+ """
261
+ payload = {
262
+ "serialNumber": self.client.serial_number,
263
+ "checkCode": self.client.check_code
264
+ }
265
+
266
+ try:
267
+ async with aiohttp.ClientSession() as session:
268
+ async with session.post(
269
+ self.client.get_endpoint(Endpoints.DETAIL),
270
+ json=payload,
271
+ headers={"Content-Type": "application/json"}
272
+ ) as response:
273
+ if response.status != 200:
274
+ print(f"Non-200 status from detail endpoint: {response.status}")
275
+ return None
276
+
277
+ # Fix for FlashForge printer's malformed Content-Type header
278
+ # Some printers return "appliation/json" instead of "application/json"
279
+ # We'll manually parse the text as JSON to bypass Content-Type validation
280
+ try:
281
+ data = await response.json()
282
+ except aiohttp.ContentTypeError:
283
+ # Fallback: manually parse as JSON if Content-Type is malformed
284
+ text = await response.text()
285
+ import json
286
+ data = json.loads(text)
287
+
288
+ return DetailResponse(**data)
289
+
290
+ except Exception as error:
291
+ print(f"GetDetailResponse Request error: {error}")
292
+ return None