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
flashforge/client.py CHANGED
@@ -4,33 +4,50 @@ FlashForge Python API - Main Unified Client
4
4
  This module provides the main FlashForgeClient class that orchestrates both HTTP and TCP
5
5
  communication layers for controlling FlashForge 3D printers.
6
6
  """
7
+
7
8
  import asyncio
8
- from typing import Optional
9
+ from dataclasses import dataclass
9
10
 
10
11
  import aiohttp
11
12
 
12
- from .api.constants.endpoints import Endpoints
13
+ from .api.constants.endpoints import CAMERA_STREAM_PORT, Endpoints
13
14
  from .api.controls import Control, Files, Info, JobControl, TempControl
14
15
  from .api.controls.info import MachineInfoParser
15
16
  from .api.network.utils import NetworkUtils
16
- from .models import FFMachineInfo, ProductResponse
17
+ from .models import FFMachineInfo, Product, ProductResponse
17
18
  from .tcp import FlashForgeClient as TcpClient
18
- from .tcp import PrinterInfo
19
+ from .tcp import FlashForgeTcpClientOptions, PrinterInfo
20
+ from .tcp.parsers.temp_info import TempInfo
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class FiveMClientConnectionOptions:
25
+ """Optional connection and feature overrides for FiveMClient-compatible clients."""
26
+
27
+ http_port: int | None = None
28
+ tcp_port: int | None = None
29
+ led_control_override: bool | None = None
19
30
 
20
31
 
21
32
  class FlashForgeClient:
22
33
  """
23
34
  Main client for interacting with a FlashForge 3D printer.
24
-
35
+
25
36
  This class provides methods for controlling the printer, managing print jobs,
26
37
  retrieving information, and handling file operations. It orchestrates both
27
38
  HTTP and TCP communication layers to provide a unified interface.
28
39
  """
29
40
 
30
- def __init__(self, ip_address: str, serial_number: str, check_code: str):
41
+ def __init__(
42
+ self,
43
+ ip_address: str,
44
+ serial_number: str,
45
+ check_code: str,
46
+ options: FiveMClientConnectionOptions | None = None,
47
+ ):
31
48
  """
32
49
  Creates an instance of FlashForgeClient.
33
-
50
+
34
51
  Args:
35
52
  ip_address: The IP address of the printer
36
53
  serial_number: The serial number of the printer
@@ -42,15 +59,19 @@ class FlashForgeClient:
42
59
  self.check_code = check_code
43
60
 
44
61
  # Constants
45
- self._PORT = 8898
62
+ self._PORT = options.http_port if options and options.http_port is not None else 8898
46
63
  self._HTTP_TIMEOUT = 5.0
47
64
 
48
65
  # HTTP client state
49
- self._http_session: Optional[aiohttp.ClientSession] = None
50
- self._http_client_busy = False
66
+ self._http_session: aiohttp.ClientSession | None = None
67
+ self._http_client_event = asyncio.Event()
68
+ self._http_client_event.set() # Not busy initially
51
69
 
52
70
  # TCP client setup
53
- self.tcp_client = TcpClient(ip_address)
71
+ tcp_options = None
72
+ if options and options.tcp_port is not None:
73
+ tcp_options = FlashForgeTcpClientOptions(port=options.tcp_port)
74
+ self.tcp_client = TcpClient(ip_address, tcp_options)
54
75
 
55
76
  # Control instances
56
77
  self.control = Control(self)
@@ -68,12 +89,22 @@ class FlashForgeClient:
68
89
  self.mac_address: str = ""
69
90
  self.flash_cloud_code: str = ""
70
91
  self.polar_cloud_code: str = ""
92
+ self.camera_stream_url: str = ""
71
93
  self.lifetime_print_time: str = ""
72
94
  self.lifetime_filament_meters: str = ""
95
+ self.product_info: Product | None = None
73
96
 
74
97
  # Control states
75
98
  self.led_control: bool = False
76
99
  self.filtration_control: bool = False
100
+ self._detected_led_control: bool = False
101
+ self._detected_filtration_control: bool = False
102
+ self._led_control_override = (
103
+ options.led_control_override
104
+ if options and options.led_control_override is not None
105
+ else None
106
+ )
107
+ self._apply_feature_overrides()
77
108
 
78
109
  @property
79
110
  def is_ad5x(self) -> bool:
@@ -85,35 +116,37 @@ class FlashForgeClient:
85
116
  """
86
117
  return self._is_ad5x
87
118
 
88
- async def __aenter__(self):
119
+ async def __aenter__(self) -> "FlashForgeClient":
89
120
  """Async context manager entry."""
90
121
  await self._ensure_http_session()
91
122
  await self.initialize()
92
123
  return self
93
124
 
94
- async def __aexit__(self, exc_type, exc_val, exc_tb):
125
+ async def __aexit__(
126
+ self,
127
+ exc_type: type[BaseException] | None,
128
+ exc_val: BaseException | None,
129
+ exc_tb: object,
130
+ ) -> None:
95
131
  """Async context manager exit."""
96
132
  await self.dispose()
97
133
 
98
134
  async def _ensure_http_session(self) -> aiohttp.ClientSession:
99
135
  """
100
136
  Ensures that an HTTP session is available.
101
-
137
+
102
138
  Returns:
103
139
  The HTTP session instance
104
140
  """
105
141
  if self._http_session is None or self._http_session.closed:
106
142
  timeout = aiohttp.ClientTimeout(total=self._HTTP_TIMEOUT)
107
- self._http_session = aiohttp.ClientSession(
108
- timeout=timeout,
109
- headers={"Accept": "*/*"}
110
- )
143
+ self._http_session = aiohttp.ClientSession(timeout=timeout, headers={"Accept": "*/*"})
111
144
  return self._http_session
112
145
 
113
146
  async def initialize(self) -> bool:
114
147
  """
115
148
  Initializes the FlashForgeClient and verifies the connection to the printer.
116
-
149
+
117
150
  Returns:
118
151
  True if initialization is successful, False otherwise
119
152
  """
@@ -123,17 +156,35 @@ class FlashForgeClient:
123
156
  print("Failed to connect to printer")
124
157
  return False
125
158
 
159
+ @property
160
+ def _http_client_busy(self) -> bool:
161
+ """
162
+ Legacy property to maintain internal API compatibility.
163
+ Returns True if the client is busy (event is not set), False otherwise.
164
+ """
165
+ return not self._http_client_event.is_set()
166
+
167
+ @_http_client_busy.setter
168
+ def _http_client_busy(self, value: bool) -> None:
169
+ """
170
+ Sets the busy state using the event.
171
+ True means busy (clear event), False means not busy (set event).
172
+ """
173
+ if value:
174
+ self._http_client_event.clear()
175
+ else:
176
+ self._http_client_event.set()
177
+
126
178
  async def is_http_client_busy(self) -> bool:
127
179
  """
128
180
  Checks if the HTTP client is currently busy.
129
-
181
+ Waits until the client is not busy before returning.
182
+
130
183
  Returns:
131
- True if the HTTP client is busy, False otherwise
184
+ False (always returns False after waiting, for compatibility)
132
185
  """
133
- # Wait a bit if busy to prevent tight loops
134
- while self._http_client_busy:
135
- await asyncio.sleep(0.01)
136
- return self._http_client_busy
186
+ await self._http_client_event.wait()
187
+ return False
137
188
 
138
189
  def release_http_client(self) -> None:
139
190
  """Releases the HTTP client, allowing it to be used for new requests."""
@@ -142,9 +193,9 @@ class FlashForgeClient:
142
193
  async def init_control(self) -> bool:
143
194
  """
144
195
  Initializes the control interface with the printer.
145
-
196
+
146
197
  This involves sending a product command and initializing TCP control.
147
-
198
+
148
199
  Returns:
149
200
  True if control initialization is successful, False otherwise
150
201
  """
@@ -155,20 +206,75 @@ class FlashForgeClient:
155
206
 
156
207
  async def dispose(self) -> None:
157
208
  """
158
- Disposes of the FlashForgeClient instance, stopping keep-alive messages
209
+ Disposes of the FlashForgeClient instance, stopping keep-alive messages
159
210
  and cleaning up resources.
160
211
  """
161
212
  # Stop TCP keep-alive and dispose
162
- if hasattr(self.tcp_client, 'stop_keep_alive'):
213
+ if hasattr(self.tcp_client, "stop_keep_alive"):
163
214
  await self.tcp_client.stop_keep_alive(True)
164
- if hasattr(self.tcp_client, 'dispose'):
215
+ if hasattr(self.tcp_client, "dispose"):
165
216
  await self.tcp_client.dispose()
166
217
 
167
218
  # Close HTTP session
168
219
  if self._http_session and not self._http_session.closed:
169
220
  await self._http_session.close()
170
221
 
171
- def cache_details(self, info: Optional[FFMachineInfo]) -> bool:
222
+ self.camera_stream_url = ""
223
+
224
+ async def detect_camera_stream(self, timeout_ms: int = 3000) -> str:
225
+ """
226
+ Probes the printer's known OEM camera endpoint.
227
+
228
+ Falls back to a short GET when HEAD is unsupported so MJPEG servers
229
+ that reject HEAD requests are still detected.
230
+
231
+ Args:
232
+ timeout_ms: Timeout for each probe attempt in milliseconds.
233
+
234
+ Returns:
235
+ The working camera stream URL, or an empty string if not detected.
236
+ """
237
+ probe_url = f"http://{self.ip_address}:{CAMERA_STREAM_PORT}/?action=stream"
238
+ timeout = aiohttp.ClientTimeout(total=timeout_ms / 1000)
239
+
240
+ if await self._probe_camera_stream_with_method("head", probe_url, timeout):
241
+ return probe_url
242
+
243
+ if await self._probe_camera_stream_with_method("get", probe_url, timeout):
244
+ return probe_url
245
+
246
+ return ""
247
+
248
+ async def _probe_camera_stream_with_method(
249
+ self,
250
+ method: str,
251
+ probe_url: str,
252
+ timeout: aiohttp.ClientTimeout,
253
+ ) -> bool:
254
+ try:
255
+ session = await self._ensure_http_session()
256
+ request = session.head if method == "head" else session.get
257
+
258
+ async with request(probe_url, timeout=timeout) as response:
259
+ return self._is_valid_camera_probe_response(
260
+ response.status, response.headers.get("Content-Type", "")
261
+ )
262
+ except Exception:
263
+ return False
264
+
265
+ @staticmethod
266
+ def _is_valid_camera_probe_response(status: int, content_type: str | None) -> bool:
267
+ if status != 200:
268
+ return False
269
+
270
+ normalized_content_type = (content_type or "").lower()
271
+ return (
272
+ normalized_content_type == ""
273
+ or "multipart" in normalized_content_type
274
+ or "video/x-mjpeg" in normalized_content_type
275
+ )
276
+
277
+ def cache_details(self, info: FFMachineInfo | None) -> bool:
172
278
  """
173
279
  Caches machine details from the provided FFMachineInfo object.
174
280
 
@@ -183,29 +289,30 @@ class FlashForgeClient:
183
289
 
184
290
  # Cache all printer information
185
291
  self.printer_name = info.name or ""
292
+ self.is_pro = info.is_pro
186
293
  self.firmware_version = info.firmware_version or ""
187
- self.firmware_ver = info.firmware_version.split('-')[0] if info.firmware_version else ""
294
+ self.firmware_ver = info.firmware_version.split("-")[0] if info.firmware_version else ""
188
295
  self.mac_address = info.mac_address or ""
189
296
  self.flash_cloud_code = info.flash_cloud_register_code or ""
190
297
  self.polar_cloud_code = info.polar_cloud_register_code or ""
298
+ self.camera_stream_url = info.camera_stream_url or ""
191
299
  self.lifetime_print_time = info.formatted_total_run_time or ""
192
300
  self._is_ad5x = info.is_ad5x
193
301
 
194
302
  # Format filament usage
195
- if info.cumulative_filament is not None:
196
- self.lifetime_filament_meters = f"{info.cumulative_filament:.2f}m"
197
- else:
198
- self.lifetime_filament_meters = "0.00m"
303
+ filament_value = info.cumulative_filament if info.cumulative_filament is not None else 0.0
304
+ self.lifetime_filament_meters = f"{filament_value:.2f}m"
305
+ self._apply_feature_overrides()
199
306
 
200
307
  return True
201
308
 
202
309
  def get_endpoint(self, endpoint: str) -> str:
203
310
  """
204
311
  Constructs the full API endpoint URL.
205
-
312
+
206
313
  Args:
207
314
  endpoint: The specific API endpoint path
208
-
315
+
209
316
  Returns:
210
317
  The full URL for the API endpoint
211
318
  """
@@ -214,7 +321,7 @@ class FlashForgeClient:
214
321
  async def verify_connection(self) -> bool:
215
322
  """
216
323
  Verifies the connection to the printer by retrieving machine details and TCP information.
217
-
324
+
218
325
  Returns:
219
326
  True if the connection is verified, False otherwise
220
327
  """
@@ -232,12 +339,14 @@ class FlashForgeClient:
232
339
  return False
233
340
 
234
341
  # Get TCP printer information to check for Pro model
235
- tcp_info: Optional[PrinterInfo] = await self.tcp_client.get_printer_info()
342
+ tcp_info: PrinterInfo | None = await self.tcp_client.get_printer_info()
236
343
  if tcp_info:
237
- if "Pro" in tcp_info.type_name:
344
+ if "Pro" in tcp_info.type_name and not machine_info.is_pro and not machine_info.is_ad5x:
238
345
  self.is_pro = True
239
346
  else:
240
- print("Warning: Unable to get PrinterInfo from TCP API, some features might not work")
347
+ print(
348
+ "Warning: Unable to get PrinterInfo from TCP API, some features might not work"
349
+ )
241
350
 
242
351
  # Cache the details
243
352
  return self.cache_details(machine_info)
@@ -249,28 +358,24 @@ class FlashForgeClient:
249
358
  async def send_product_command(self) -> bool:
250
359
  """
251
360
  Sends a product command to the printer to retrieve control states.
252
-
361
+
253
362
  This method sets the http_client_busy flag while the request is in progress.
254
-
363
+
255
364
  Returns:
256
- True if the product command is sent successfully and valid data is received,
365
+ True if the product command is sent successfully and valid data is received,
257
366
  False otherwise
258
367
  """
259
368
  self._http_client_busy = True
260
369
 
261
- payload = {
262
- "serialNumber": self.serial_number,
263
- "checkCode": self.check_code
264
- }
370
+ payload = {"serialNumber": self.serial_number, "checkCode": self.check_code}
265
371
 
266
372
  try:
267
373
  session = await self._ensure_http_session()
268
374
  async with session.post(
269
375
  self.get_endpoint(Endpoints.PRODUCT),
270
376
  json=payload,
271
- headers={"Content-Type": "application/json"}
377
+ headers={"Content-Type": "application/json"},
272
378
  ) as response:
273
-
274
379
  if response.status != 200:
275
380
  return False
276
381
 
@@ -282,6 +387,7 @@ class FlashForgeClient:
282
387
  # Fallback: manually parse as JSON if Content-Type is malformed
283
388
  text = await response.text()
284
389
  import json
390
+
285
391
  data = json.loads(text)
286
392
 
287
393
  # Validate response structure
@@ -292,11 +398,12 @@ class FlashForgeClient:
292
398
  product_response = ProductResponse(**data)
293
399
  if product_response and product_response.product:
294
400
  product = product_response.product
295
- self.led_control = product.lightCtrlState != 0
296
- self.filtration_control = not (
297
- product.internalFanCtrlState == 0 or
298
- product.externalFanCtrlState == 0
401
+ self.product_info = product
402
+ self._detected_led_control = product.lightCtrlState != 0
403
+ self._detected_filtration_control = not (
404
+ product.internalFanCtrlState == 0 or product.externalFanCtrlState == 0
299
405
  )
406
+ self._apply_feature_overrides()
300
407
  return True
301
408
 
302
409
  except Exception as error:
@@ -310,7 +417,7 @@ class FlashForgeClient:
310
417
  async def get_http_session(self) -> aiohttp.ClientSession:
311
418
  """
312
419
  Gets the HTTP session for making requests.
313
-
420
+
314
421
  Returns:
315
422
  The HTTP session instance
316
423
  """
@@ -318,19 +425,19 @@ class FlashForgeClient:
318
425
 
319
426
  # Additional convenience methods for direct access to common operations
320
427
 
321
- async def get_printer_status(self) -> Optional[FFMachineInfo]:
428
+ async def get_printer_status(self) -> FFMachineInfo | None:
322
429
  """
323
430
  Gets the current printer status and information.
324
-
431
+
325
432
  Returns:
326
433
  FFMachineInfo object with current printer status, or None if failed
327
434
  """
328
435
  return await self.info.get()
329
436
 
330
- async def get_temperatures(self):
437
+ async def get_temperatures(self) -> TempInfo | None:
331
438
  """
332
439
  Gets current temperature readings from the printer.
333
-
440
+
334
441
  Returns:
335
442
  Temperature information from the TCP client
336
443
  """
@@ -339,7 +446,7 @@ class FlashForgeClient:
339
446
  async def home_all_axes(self) -> bool:
340
447
  """
341
448
  Homes all axes (X, Y, Z) of the printer.
342
-
449
+
343
450
  Returns:
344
451
  True if successful, False otherwise
345
452
  """
@@ -348,7 +455,7 @@ class FlashForgeClient:
348
455
  async def emergency_stop(self) -> bool:
349
456
  """
350
457
  Performs an emergency stop of the printer.
351
-
458
+
352
459
  Returns:
353
460
  True if successful, False otherwise
354
461
  """
@@ -357,7 +464,7 @@ class FlashForgeClient:
357
464
  async def pause_print(self) -> bool:
358
465
  """
359
466
  Pauses the current print job.
360
-
467
+
361
468
  Returns:
362
469
  True if successful, False otherwise
363
470
  """
@@ -366,12 +473,26 @@ class FlashForgeClient:
366
473
  async def resume_print(self) -> bool:
367
474
  """
368
475
  Resumes a paused print job.
369
-
476
+
370
477
  Returns:
371
478
  True if successful, False otherwise
372
479
  """
373
480
  return await self.job_control.resume_print_job()
374
481
 
482
+ def set_feature_overrides(self, *, led_control: bool | None = None) -> None:
483
+ """Apply manual feature overrides for integrations that need UI-level capability control."""
484
+ self._led_control_override = led_control
485
+ self._apply_feature_overrides()
486
+
487
+ def _apply_feature_overrides(self) -> None:
488
+ """Apply any configured manual capability overrides to the cached client state."""
489
+ self.led_control = (
490
+ self._led_control_override
491
+ if self._led_control_override is not None
492
+ else self._detected_led_control
493
+ )
494
+ self.filtration_control = self._detected_filtration_control
495
+
375
496
  def __repr__(self) -> str:
376
497
  """String representation of the client."""
377
498
  return (
@@ -4,9 +4,36 @@ FlashForge Python API - Discovery Package
4
4
  UDP-based printer discovery for finding FlashForge printers on the local network.
5
5
  """
6
6
 
7
- from .discovery import FlashForgePrinter, FlashForgePrinterDiscovery
7
+ from .discovery import (
8
+ DiscoveredPrinter,
9
+ DiscoveryError,
10
+ DiscoveryMonitor,
11
+ DiscoveryOptions,
12
+ DiscoveryProtocol,
13
+ DiscoveryTimeoutError,
14
+ FlashForgePrinter,
15
+ FlashForgePrinterDiscovery,
16
+ InvalidResponseError,
17
+ PrinterDiscovery,
18
+ PrinterModel,
19
+ PrinterStatus,
20
+ SocketCreationError,
21
+ main,
22
+ )
8
23
 
9
24
  __all__ = [
10
- 'FlashForgePrinter',
11
- 'FlashForgePrinterDiscovery',
25
+ "PrinterDiscovery",
26
+ "DiscoveredPrinter",
27
+ "DiscoveryOptions",
28
+ "PrinterModel",
29
+ "DiscoveryProtocol",
30
+ "PrinterStatus",
31
+ "DiscoveryMonitor",
32
+ "DiscoveryError",
33
+ "InvalidResponseError",
34
+ "SocketCreationError",
35
+ "DiscoveryTimeoutError",
36
+ "FlashForgePrinter",
37
+ "FlashForgePrinterDiscovery",
38
+ "main",
12
39
  ]