flashforge-python-api 1.3.1__py3-none-any.whl → 1.3.3__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.
@@ -2,6 +2,7 @@
2
2
  FlashForge Python API - Control Module
3
3
  """
4
4
 
5
+ import logging
5
6
  from typing import TYPE_CHECKING, Any
6
7
 
7
8
  from ...models.responses import FilamentArgs
@@ -14,6 +15,8 @@ if TYPE_CHECKING:
14
15
  from ...client import FlashForgeClient
15
16
  from ...tcp.ff_client import FlashForgeClient as TcpClient
16
17
 
18
+ logger = logging.getLogger(__name__)
19
+
17
20
 
18
21
  class Control:
19
22
  """
@@ -75,7 +78,7 @@ class Control:
75
78
  return await self._send_filtration_command(
76
79
  FilamentArgs(internal="close", external="open")
77
80
  )
78
- print("SetExternalFiltrationOn() error, filtration not equipped.")
81
+ logger.warning("set_external_filtration_on: filtration is not equipped on this printer.")
79
82
  return False
80
83
 
81
84
  async def set_internal_filtration_on(self) -> bool:
@@ -90,7 +93,7 @@ class Control:
90
93
  return await self._send_filtration_command(
91
94
  FilamentArgs(internal="open", external="close")
92
95
  )
93
- print("SetInternalFiltrationOn() error, filtration not equipped.")
96
+ logger.warning("set_internal_filtration_on: filtration is not equipped on this printer.")
94
97
  return False
95
98
 
96
99
  async def set_filtration_off(self) -> bool:
@@ -105,7 +108,7 @@ class Control:
105
108
  return await self._send_filtration_command(
106
109
  FilamentArgs(internal="close", external="close")
107
110
  )
108
- print("SetFiltrationOff() error, filtration not equipped.")
111
+ logger.warning("set_filtration_off: filtration is not equipped on this printer.")
109
112
  return False
110
113
 
111
114
  async def turn_camera_on(self) -> bool:
@@ -188,7 +191,7 @@ class Control:
188
191
  True if the command is successful, False otherwise.
189
192
  """
190
193
  if not self.client.led_control:
191
- print("SetLedOn() error, LED control not equipped.")
194
+ logger.warning("set_led_on: LED control is not equipped on this printer.")
192
195
  return False
193
196
  return await self.send_control_command(Commands.LIGHT_CONTROL_CMD, {"status": "open"})
194
197
 
@@ -200,7 +203,7 @@ class Control:
200
203
  True if the command is successful, False otherwise.
201
204
  """
202
205
  if not self.client.led_control:
203
- print("SetLedOff() error, LED control not equipped.")
206
+ logger.warning("set_led_off: LED control is not equipped on this printer.")
204
207
  return False
205
208
  return await self.send_control_command(Commands.LIGHT_CONTROL_CMD, {"status": "close"})
206
209
 
@@ -259,7 +262,9 @@ class Control:
259
262
  True if the command is successful, False otherwise.
260
263
  """
261
264
  if not self.client.is_ad5x and not self.client.is_creator5:
262
- print("configure_slot() error, material station only available on AD5X / Creator 5.")
265
+ logger.warning(
266
+ "configure_slot: the material station is only available on the AD5X / Creator 5."
267
+ )
263
268
  return False
264
269
  # The AD5X and Creator 5 use MUTUALLY EXCLUSIVE color wire formats (see
265
270
  # creator5_palette for the firmware match rules), so model-gate here:
@@ -293,7 +298,9 @@ class Control:
293
298
  "payload": {"cmd": command, "args": args},
294
299
  }
295
300
 
296
- print(f"SendControlCommand:\n{payload}")
301
+ # Log the command, never `payload`: it carries the serial number and
302
+ # check code, and these lines end up pasted into bug reports.
303
+ logger.debug("Sending control command %s with args %s", command, args)
297
304
 
298
305
  try:
299
306
  await self.client.is_http_client_busy()
@@ -305,12 +312,12 @@ class Control:
305
312
  headers={"Content-Type": "application/json"},
306
313
  ) as response:
307
314
  data = await json_from_response(response)
308
- print(f"Command reply: {data}")
315
+ logger.debug("Control command %s reply: %s", command, data)
309
316
 
310
317
  return NetworkUtils.is_ok(data)
311
318
 
312
319
  except Exception as e:
313
- print(f"Error in send_control_command: {e}")
320
+ logger.warning("Error in send_control_command (%s): %s", command, e)
314
321
  return False
315
322
  finally:
316
323
  self.client.release_http_client()
@@ -16,6 +16,7 @@ for the model-gating that splits them.
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import logging
19
20
  import math
20
21
  import re
21
22
  from dataclasses import dataclass
@@ -25,6 +26,8 @@ from dataclasses import dataclass
25
26
  # reference. PEP 8 lowercase locals are therefore relaxed here on purpose.
26
27
  # ruff: noqa: N806
27
28
 
29
+ logger = logging.getLogger(__name__)
30
+
28
31
 
29
32
  @dataclass(frozen=True)
30
33
  class Creator5PaletteColor:
@@ -70,7 +73,7 @@ CREATOR5_PALETTE: tuple[Creator5PaletteColor, ...] = (
70
73
  )
71
74
 
72
75
  # 25^7, the CIEDE2000 chroma weighting constant.
73
- _25_POW_7 = 25.0 ** 7
76
+ _25_POW_7 = 25.0**7
74
77
 
75
78
  # D65 reference white point used by the sRGB -> XYZ transform.
76
79
  _D65_XN = 0.95047
@@ -132,7 +135,7 @@ def _delta_e_2000(c1: tuple[float, float, float], c2: tuple[float, float, float]
132
135
  C1 = math.sqrt(a1 * a1 + b1 * b1)
133
136
  C2 = math.sqrt(a2 * a2 + b2 * b2)
134
137
  Cbar = (C1 + C2) / 2.0
135
- Cbar7 = Cbar ** 7
138
+ Cbar7 = Cbar**7
136
139
  G = 0.5 * (1 - math.sqrt(Cbar7 / (Cbar7 + _25_POW_7)))
137
140
 
138
141
  a1p = (1 + G) * a1
@@ -178,7 +181,7 @@ def _delta_e_2000(c1: tuple[float, float, float], c2: tuple[float, float, float]
178
181
  )
179
182
 
180
183
  dTheta = 30 * math.exp(-(((hbarp - 275) / 25.0) ** 2))
181
- Cbarp7 = Cbarp ** 7
184
+ Cbarp7 = Cbarp**7
182
185
  RC = 2 * math.sqrt(Cbarp7 / (Cbarp7 + _25_POW_7))
183
186
  SL = 1 + (0.015 * (Lbarp - 50) ** 2) / math.sqrt(20 + (Lbarp - 50) ** 2)
184
187
  SC = 1 + 0.045 * Cbarp
@@ -238,9 +241,9 @@ def snap_to_creator5_palette(hex_str: str) -> Creator5PaletteColor:
238
241
  """
239
242
  rgb = _hex_to_rgb(hex_str)
240
243
  if rgb is None:
241
- print(
242
- f'snap_to_creator5_palette: could not parse "{hex_str}" as hex; '
243
- "falling back to White."
244
+ logger.warning(
245
+ 'snap_to_creator5_palette: could not parse "%s" as hex; falling back to White.',
246
+ hex_str,
244
247
  )
245
248
  return CREATOR5_PALETTE[0]
246
249
 
@@ -3,6 +3,7 @@ FlashForge Python API - Files Module
3
3
  """
4
4
 
5
5
  import base64
6
+ import logging
6
7
  from typing import TYPE_CHECKING
7
8
 
8
9
  from pydantic import ValidationError
@@ -15,6 +16,8 @@ from ..network.utils import NetworkUtils, json_from_response
15
16
  if TYPE_CHECKING:
16
17
  from ...client import FlashForgeClient
17
18
 
19
+ logger = logging.getLogger(__name__)
20
+
18
21
 
19
22
  class Files:
20
23
  """
@@ -87,13 +90,27 @@ class Files:
87
90
  data = await json_from_response(response)
88
91
 
89
92
  if not NetworkUtils.is_ok(data):
90
- print(f"Error retrieving file list: {NetworkUtils.get_error_message(data)}")
93
+ logger.warning(
94
+ "Error retrieving the file list: %s",
95
+ NetworkUtils.get_error_message(data),
96
+ )
91
97
  return []
92
98
 
93
99
  # Parse the response using GCodeListResponse
94
100
  try:
95
101
  result = GCodeListResponse(**data)
96
- except ValidationError:
102
+ except ValidationError as err:
103
+ # The names-only fallback below silently costs the caller
104
+ # every per-file field, which is indistinguishable from a
105
+ # printer that only reports names. Say so, loudly enough to
106
+ # reach an integration's log, or the next model that changes
107
+ # this payload looks like it reports no metadata at all.
108
+ logger.warning(
109
+ "Could not parse the /gcodeList response; falling back to file "
110
+ "names only, so print time, filament weight, and per-tool "
111
+ "material data are unavailable for every file. %s",
112
+ err,
113
+ )
97
114
  raw_list = data.get("gcodeList", [])
98
115
  if isinstance(raw_list, list):
99
116
  entries: list[FFGcodeFileEntry] = []
@@ -127,15 +144,13 @@ class Files:
127
144
  elif isinstance(first_item, FFGcodeFileEntry):
128
145
  # Already FFGcodeFileEntry objects - need explicit type narrowing
129
146
  return [
130
- item
131
- for item in result.gcode_list
132
- if isinstance(item, FFGcodeFileEntry)
147
+ item for item in result.gcode_list if isinstance(item, FFGcodeFileEntry)
133
148
  ]
134
149
 
135
150
  return []
136
151
 
137
152
  except Exception as err:
138
- print(f"GetRecentFileList error: {err}")
153
+ logger.warning("get_recent_file_list error: %s", err)
139
154
  return []
140
155
 
141
156
  async def get_gcode_thumbnail(self, file_name: str) -> bytes | None:
@@ -173,9 +188,12 @@ class Files:
173
188
  result = ThumbnailResponse(**data)
174
189
  return base64.b64decode(result.image_data)
175
190
  else:
176
- print(f"Error retrieving thumbnail: {NetworkUtils.get_error_message(data)}")
191
+ logger.warning(
192
+ "Error retrieving the thumbnail: %s",
193
+ NetworkUtils.get_error_message(data),
194
+ )
177
195
  return None
178
196
 
179
197
  except Exception as err:
180
- print(f"GetGcodeThumbnail error: {err}")
198
+ logger.warning("get_gcode_thumbnail error: %s", err)
181
199
  return None
@@ -2,6 +2,7 @@
2
2
  FlashForge Python API - Info Module
3
3
  """
4
4
 
5
+ import logging
5
6
  import re
6
7
  from datetime import datetime, timedelta
7
8
  from typing import TYPE_CHECKING
@@ -9,11 +10,14 @@ from typing import TYPE_CHECKING
9
10
  from ...models.machine_info import FFMachineInfo, MachineState, Temperature
10
11
  from ...models.responses import DetailResponse, FFPrinterDetail
11
12
  from ..constants.endpoints import Endpoints
13
+ from ..misc.redaction import redact_model
12
14
  from ..network.utils import json_from_response
13
15
 
14
16
  if TYPE_CHECKING:
15
17
  from ...client import FlashForgeClient
16
18
 
19
+ logger = logging.getLogger(__name__)
20
+
17
21
 
18
22
  # Firmware-reported PIDs from FlashForge's /detail endpoint. These are stable
19
23
  # identifiers set by firmware, unlike the user-mutable `name` field.
@@ -89,10 +93,17 @@ class MachineInfoParser:
89
93
  print_progress = getattr(detail, "print_progress", 0) or 0
90
94
  est_length = total_job_filament_meters * print_progress
91
95
  est_weight = (getattr(detail, "estimated_right_weight", 0) or 0) * print_progress
96
+ # Material Station presence, derived rather than read off a single
97
+ # field. `hasMatlStation` is AD5X-only: the Creator 5 series omits it
98
+ # from /detail entirely (verified on a Creator 5 Pro, pid 41, firmware
99
+ # 1.9.4) while reporting a fully populated matlStationInfo with four
100
+ # loaded slots, so an absent flag means "not reported", never "absent
101
+ # hardware". Populated slot data is the reliable signal.
92
102
  has_material_station = (
93
103
  getattr(detail, "has_matl_station", None) is True
94
104
  or (getattr(getattr(detail, "matl_station_info", None), "slot_cnt", 0) or 0) > 0
95
- or len(getattr(getattr(detail, "matl_station_info", None), "slot_infos", []) or []) > 0
105
+ or len(getattr(getattr(detail, "matl_station_info", None), "slot_infos", []) or [])
106
+ > 0
96
107
  )
97
108
  printer_name = getattr(detail, "name", "") or ""
98
109
  pid = getattr(detail, "pid", None)
@@ -236,7 +247,9 @@ class MachineInfoParser:
236
247
  print_speed_adjust=getattr(detail, "print_speed_adjust", 0) or 0,
237
248
  filament_type=getattr(detail, "right_filament_type", "") or "",
238
249
  # Machine state
239
- machine_state=MachineInfoParser._get_machine_state(getattr(detail, "status", "") or ""),
250
+ machine_state=MachineInfoParser._get_machine_state(
251
+ getattr(detail, "status", "") or ""
252
+ ),
240
253
  status=getattr(detail, "status", "") or "",
241
254
  total_print_layers=getattr(detail, "target_print_layer", 0) or 0,
242
255
  tvoc=getattr(detail, "tvoc", 0) or 0,
@@ -249,8 +262,8 @@ class MachineInfoParser:
249
262
  completion_time=completion_time,
250
263
  formatted_run_time=formatted_run_time,
251
264
  formatted_total_run_time=formatted_total_run_time,
252
- # AD5X Material Station
253
- has_matl_station=getattr(detail, "has_matl_station", None),
265
+ # Material Station (AD5X / Creator 5 series)
266
+ has_matl_station=has_material_station,
254
267
  matl_station_info=getattr(detail, "matl_station_info", None),
255
268
  indep_matl_info=getattr(detail, "indep_matl_info", None),
256
269
  )
@@ -258,8 +271,14 @@ class MachineInfoParser:
258
271
  return machine_info
259
272
 
260
273
  except Exception as error:
261
- print(f"Error in MachineInfoParser.from_detail: {error}")
262
- print(f"Detail object causing error: {detail}")
274
+ logger.warning(
275
+ "Could not parse the /detail payload into FFMachineInfo; the caller sees "
276
+ "this as an unreachable printer. %s",
277
+ error,
278
+ )
279
+ # Redacted: this dump carries the MAC, IP and cloud registration
280
+ # codes, and it exists to be pasted into a bug report.
281
+ logger.debug("Detail object that failed to parse: %s", redact_model(detail))
263
282
  return None
264
283
 
265
284
  @staticmethod
@@ -294,7 +313,7 @@ class MachineInfoParser:
294
313
  return state_mapping[valid_status]
295
314
 
296
315
  if valid_status:
297
- print(f"Unknown machine status received: '{status}'")
316
+ logger.warning("Unknown machine status received: %r", status)
298
317
  return MachineState.UNKNOWN
299
318
 
300
319
 
@@ -375,12 +394,17 @@ class Info:
375
394
  headers={"Content-Type": "application/json"},
376
395
  ) as response:
377
396
  if response.status != 200:
378
- print(f"Non-200 status from detail endpoint: {response.status}")
397
+ logger.warning("Non-200 status from the /detail endpoint: %s", response.status)
379
398
  return None
380
399
 
381
400
  data = await json_from_response(response)
382
401
  return DetailResponse(**data)
383
402
 
384
403
  except Exception as error:
385
- print(f"GetDetailResponse Request error: {error}")
404
+ logger.warning(
405
+ "Could not read /detail; callers cannot tell this from an unreachable "
406
+ "printer or a rejected check code, so this line is the only record of "
407
+ "the real cause. %s",
408
+ error,
409
+ )
386
410
  return None