flashforge-python-api 1.0.2__py3-none-any.whl → 1.1.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 (41) hide show
  1. flashforge/__init__.py +62 -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 +1 -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 +131 -63
  16. flashforge/discovery/__init__.py +30 -3
  17. flashforge/discovery/discovery.py +637 -308
  18. flashforge/models/__init__.py +11 -10
  19. flashforge/models/machine_info.py +220 -100
  20. flashforge/models/responses.py +103 -43
  21. flashforge/tcp/__init__.py +25 -17
  22. flashforge/tcp/a3_client.py +405 -0
  23. flashforge/tcp/ff_client.py +93 -90
  24. flashforge/tcp/gcode/__init__.py +4 -2
  25. flashforge/tcp/gcode/a3_gcode_controller.py +109 -0
  26. flashforge/tcp/gcode/gcode_controller.py +48 -36
  27. flashforge/tcp/gcode/gcodes.py +7 -1
  28. flashforge/tcp/parsers/__init__.py +11 -11
  29. flashforge/tcp/parsers/endstop_status.py +103 -132
  30. flashforge/tcp/parsers/location_info.py +26 -13
  31. flashforge/tcp/parsers/print_status.py +49 -56
  32. flashforge/tcp/parsers/printer_info.py +38 -48
  33. flashforge/tcp/parsers/temp_info.py +46 -47
  34. flashforge/tcp/parsers/thumbnail_info.py +65 -40
  35. flashforge/tcp/tcp_client.py +296 -293
  36. {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.1.0.dist-info}/METADATA +35 -14
  37. flashforge_python_api-1.1.0.dist-info/RECORD +45 -0
  38. {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.1.0.dist-info}/WHEEL +1 -1
  39. flashforge_python_api-1.0.2.dist-info/RECORD +0 -43
  40. {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.1.0.dist-info}/entry_points.txt +0 -0
  41. {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.1.0.dist-info}/licenses/LICENSE +0 -0
flashforge/client.py CHANGED
@@ -4,8 +4,9 @@ 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
 
@@ -13,24 +14,40 @@ from .api.constants.endpoints import 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,22 @@ 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
+ def cache_details(self, info: FFMachineInfo | None) -> bool:
172
225
  """
173
226
  Caches machine details from the provided FFMachineInfo object.
174
227
 
@@ -183,29 +236,30 @@ class FlashForgeClient:
183
236
 
184
237
  # Cache all printer information
185
238
  self.printer_name = info.name or ""
239
+ self.is_pro = info.is_pro
186
240
  self.firmware_version = info.firmware_version or ""
187
- self.firmware_ver = info.firmware_version.split('-')[0] if info.firmware_version else ""
241
+ self.firmware_ver = info.firmware_version.split("-")[0] if info.firmware_version else ""
188
242
  self.mac_address = info.mac_address or ""
189
243
  self.flash_cloud_code = info.flash_cloud_register_code or ""
190
244
  self.polar_cloud_code = info.polar_cloud_register_code or ""
245
+ self.camera_stream_url = info.camera_stream_url or ""
191
246
  self.lifetime_print_time = info.formatted_total_run_time or ""
192
247
  self._is_ad5x = info.is_ad5x
193
248
 
194
249
  # 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"
250
+ filament_value = info.cumulative_filament if info.cumulative_filament is not None else 0.0
251
+ self.lifetime_filament_meters = f"{filament_value:.2f}m"
252
+ self._apply_feature_overrides()
199
253
 
200
254
  return True
201
255
 
202
256
  def get_endpoint(self, endpoint: str) -> str:
203
257
  """
204
258
  Constructs the full API endpoint URL.
205
-
259
+
206
260
  Args:
207
261
  endpoint: The specific API endpoint path
208
-
262
+
209
263
  Returns:
210
264
  The full URL for the API endpoint
211
265
  """
@@ -214,7 +268,7 @@ class FlashForgeClient:
214
268
  async def verify_connection(self) -> bool:
215
269
  """
216
270
  Verifies the connection to the printer by retrieving machine details and TCP information.
217
-
271
+
218
272
  Returns:
219
273
  True if the connection is verified, False otherwise
220
274
  """
@@ -232,12 +286,14 @@ class FlashForgeClient:
232
286
  return False
233
287
 
234
288
  # Get TCP printer information to check for Pro model
235
- tcp_info: Optional[PrinterInfo] = await self.tcp_client.get_printer_info()
289
+ tcp_info: PrinterInfo | None = await self.tcp_client.get_printer_info()
236
290
  if tcp_info:
237
- if "Pro" in tcp_info.type_name:
291
+ if "Pro" in tcp_info.type_name and not machine_info.is_pro and not machine_info.is_ad5x:
238
292
  self.is_pro = True
239
293
  else:
240
- print("Warning: Unable to get PrinterInfo from TCP API, some features might not work")
294
+ print(
295
+ "Warning: Unable to get PrinterInfo from TCP API, some features might not work"
296
+ )
241
297
 
242
298
  # Cache the details
243
299
  return self.cache_details(machine_info)
@@ -249,28 +305,24 @@ class FlashForgeClient:
249
305
  async def send_product_command(self) -> bool:
250
306
  """
251
307
  Sends a product command to the printer to retrieve control states.
252
-
308
+
253
309
  This method sets the http_client_busy flag while the request is in progress.
254
-
310
+
255
311
  Returns:
256
- True if the product command is sent successfully and valid data is received,
312
+ True if the product command is sent successfully and valid data is received,
257
313
  False otherwise
258
314
  """
259
315
  self._http_client_busy = True
260
316
 
261
- payload = {
262
- "serialNumber": self.serial_number,
263
- "checkCode": self.check_code
264
- }
317
+ payload = {"serialNumber": self.serial_number, "checkCode": self.check_code}
265
318
 
266
319
  try:
267
320
  session = await self._ensure_http_session()
268
321
  async with session.post(
269
322
  self.get_endpoint(Endpoints.PRODUCT),
270
323
  json=payload,
271
- headers={"Content-Type": "application/json"}
324
+ headers={"Content-Type": "application/json"},
272
325
  ) as response:
273
-
274
326
  if response.status != 200:
275
327
  return False
276
328
 
@@ -282,6 +334,7 @@ class FlashForgeClient:
282
334
  # Fallback: manually parse as JSON if Content-Type is malformed
283
335
  text = await response.text()
284
336
  import json
337
+
285
338
  data = json.loads(text)
286
339
 
287
340
  # Validate response structure
@@ -292,11 +345,12 @@ class FlashForgeClient:
292
345
  product_response = ProductResponse(**data)
293
346
  if product_response and product_response.product:
294
347
  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
348
+ self.product_info = product
349
+ self._detected_led_control = product.lightCtrlState != 0
350
+ self._detected_filtration_control = not (
351
+ product.internalFanCtrlState == 0 or product.externalFanCtrlState == 0
299
352
  )
353
+ self._apply_feature_overrides()
300
354
  return True
301
355
 
302
356
  except Exception as error:
@@ -310,7 +364,7 @@ class FlashForgeClient:
310
364
  async def get_http_session(self) -> aiohttp.ClientSession:
311
365
  """
312
366
  Gets the HTTP session for making requests.
313
-
367
+
314
368
  Returns:
315
369
  The HTTP session instance
316
370
  """
@@ -318,19 +372,19 @@ class FlashForgeClient:
318
372
 
319
373
  # Additional convenience methods for direct access to common operations
320
374
 
321
- async def get_printer_status(self) -> Optional[FFMachineInfo]:
375
+ async def get_printer_status(self) -> FFMachineInfo | None:
322
376
  """
323
377
  Gets the current printer status and information.
324
-
378
+
325
379
  Returns:
326
380
  FFMachineInfo object with current printer status, or None if failed
327
381
  """
328
382
  return await self.info.get()
329
383
 
330
- async def get_temperatures(self):
384
+ async def get_temperatures(self) -> TempInfo | None:
331
385
  """
332
386
  Gets current temperature readings from the printer.
333
-
387
+
334
388
  Returns:
335
389
  Temperature information from the TCP client
336
390
  """
@@ -339,7 +393,7 @@ class FlashForgeClient:
339
393
  async def home_all_axes(self) -> bool:
340
394
  """
341
395
  Homes all axes (X, Y, Z) of the printer.
342
-
396
+
343
397
  Returns:
344
398
  True if successful, False otherwise
345
399
  """
@@ -348,7 +402,7 @@ class FlashForgeClient:
348
402
  async def emergency_stop(self) -> bool:
349
403
  """
350
404
  Performs an emergency stop of the printer.
351
-
405
+
352
406
  Returns:
353
407
  True if successful, False otherwise
354
408
  """
@@ -357,7 +411,7 @@ class FlashForgeClient:
357
411
  async def pause_print(self) -> bool:
358
412
  """
359
413
  Pauses the current print job.
360
-
414
+
361
415
  Returns:
362
416
  True if successful, False otherwise
363
417
  """
@@ -366,12 +420,26 @@ class FlashForgeClient:
366
420
  async def resume_print(self) -> bool:
367
421
  """
368
422
  Resumes a paused print job.
369
-
423
+
370
424
  Returns:
371
425
  True if successful, False otherwise
372
426
  """
373
427
  return await self.job_control.resume_print_job()
374
428
 
429
+ def set_feature_overrides(self, *, led_control: bool | None = None) -> None:
430
+ """Apply manual feature overrides for integrations that need UI-level capability control."""
431
+ self._led_control_override = led_control
432
+ self._apply_feature_overrides()
433
+
434
+ def _apply_feature_overrides(self) -> None:
435
+ """Apply any configured manual capability overrides to the cached client state."""
436
+ self.led_control = (
437
+ self._led_control_override
438
+ if self._led_control_override is not None
439
+ else self._detected_led_control
440
+ )
441
+ self.filtration_control = self._detected_filtration_control
442
+
375
443
  def __repr__(self) -> str:
376
444
  """String representation of the client."""
377
445
  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
  ]