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.
- flashforge/api/controls/control.py +16 -9
- flashforge/api/controls/creator5_palette.py +9 -6
- flashforge/api/controls/files.py +26 -8
- flashforge/api/controls/info.py +33 -9
- flashforge/api/controls/job_control.py +109 -71
- flashforge/api/controls/temp_control.py +14 -7
- flashforge/api/misc/redaction.py +88 -0
- flashforge/client.py +27 -10
- flashforge/discovery/discovery.py +10 -6
- flashforge/models/machine_info.py +40 -7
- flashforge/models/responses.py +22 -17
- flashforge/tcp/a3_client.py +27 -8
- flashforge/tcp/a4_client.py +5 -1
- flashforge/tcp/parsers/endstop_status.py +10 -6
- flashforge/tcp/parsers/location_info.py +3 -1
- flashforge/tcp/parsers/print_status.py +8 -3
- flashforge/tcp/parsers/printer_info.py +3 -1
- flashforge/tcp/parsers/temp_info.py +3 -1
- flashforge/tcp/parsers/thumbnail_info.py +19 -12
- flashforge/tcp/tcp_client.py +6 -5
- {flashforge_python_api-1.3.1.dist-info → flashforge_python_api-1.3.3.dist-info}/METADATA +1 -1
- flashforge_python_api-1.3.3.dist-info/RECORD +48 -0
- {flashforge_python_api-1.3.1.dist-info → flashforge_python_api-1.3.3.dist-info}/WHEEL +1 -1
- flashforge_python_api-1.3.1.dist-info/RECORD +0 -47
- {flashforge_python_api-1.3.1.dist-info → flashforge_python_api-1.3.3.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.3.1.dist-info → flashforge_python_api-1.3.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
flashforge/api/controls/files.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
+
logger.warning("get_gcode_thumbnail error: %s", err)
|
|
181
199
|
return None
|
flashforge/api/controls/info.py
CHANGED
|
@@ -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 [])
|
|
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(
|
|
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
|
|
253
|
-
has_matl_station=
|
|
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
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|