flashforge-python-api 1.3.2__py3-none-any.whl → 1.3.4__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/__init__.py +5 -1
- flashforge/api/controls/control.py +16 -9
- flashforge/api/controls/creator5_palette.py +6 -3
- flashforge/api/controls/files.py +10 -4
- flashforge/api/controls/info.py +123 -21
- flashforge/api/controls/job_control.py +106 -68
- flashforge/api/controls/temp_control.py +15 -6
- flashforge/api/misc/redaction.py +88 -0
- flashforge/client.py +9 -9
- flashforge/exceptions.py +51 -0
- flashforge/models/machine_info.py +196 -99
- flashforge/tcp/parsers/endstop_status.py +7 -5
- flashforge/tcp/parsers/print_status.py +5 -2
- flashforge/tcp/parsers/thumbnail_info.py +18 -11
- {flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.4.dist-info}/METADATA +1 -1
- {flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.4.dist-info}/RECORD +19 -17
- {flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.4.dist-info}/WHEEL +0 -0
- {flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.4.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.3.2.dist-info → flashforge_python_api-1.3.4.dist-info}/licenses/LICENSE +0 -0
flashforge/exceptions.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlashForge Python API - Exceptions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FlashForgeError(Exception):
|
|
9
|
+
"""Base class for every error this library raises deliberately."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FlashForgeResponseError(FlashForgeError):
|
|
13
|
+
"""
|
|
14
|
+
The printer answered, but the payload could not be understood.
|
|
15
|
+
|
|
16
|
+
This is deliberately distinct from a ``None`` return, which every HTTP
|
|
17
|
+
control method uses for "could not reach the printer / the printer refused
|
|
18
|
+
us". Collapsing the two is what made issue #18 take three releases to
|
|
19
|
+
diagnose: a Creator 5 reporting ``chamberTemp: -108`` (the firmware's "no
|
|
20
|
+
chamber sensor" sentinel) surfaced in Home Assistant as ``cannot_connect``,
|
|
21
|
+
pointing the user at their network for a schema problem.
|
|
22
|
+
|
|
23
|
+
Raise this whenever the transport succeeded and the *content* was the
|
|
24
|
+
problem, so callers can tell the user something actionable.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
message: str,
|
|
30
|
+
*,
|
|
31
|
+
endpoint: str | None = None,
|
|
32
|
+
cause: Exception | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Args:
|
|
36
|
+
message: Human-readable description of what could not be parsed.
|
|
37
|
+
endpoint: The endpoint whose response failed, e.g. "/detail".
|
|
38
|
+
cause: The underlying validation/decode error, kept for logs.
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.message = message
|
|
42
|
+
self.endpoint = endpoint
|
|
43
|
+
self.cause = cause
|
|
44
|
+
|
|
45
|
+
def __str__(self) -> str:
|
|
46
|
+
parts = [self.message]
|
|
47
|
+
if self.endpoint:
|
|
48
|
+
parts.append(f"(endpoint: {self.endpoint})")
|
|
49
|
+
if self.cause:
|
|
50
|
+
parts.append(f"caused by {type(self.cause).__name__}: {self.cause}")
|
|
51
|
+
return " ".join(parts)
|
|
@@ -4,11 +4,76 @@ FlashForge Python API - Data Models
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import re
|
|
7
8
|
from datetime import datetime
|
|
8
9
|
from enum import Enum
|
|
10
|
+
from typing import Any
|
|
9
11
|
|
|
10
12
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
11
13
|
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Validation policy for inbound firmware payloads
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
#
|
|
18
|
+
# Models parsed FROM the printer carry types but NOT value ranges, and no
|
|
19
|
+
# required fields beyond what we genuinely cannot work without. The reason is
|
|
20
|
+
# blast radius: pydantic fails a model as a unit, and `Info.get_detail_response`
|
|
21
|
+
# turns any failure into "no data", which Home Assistant shows as an offline
|
|
22
|
+
# printer. So a `ge=0` on a field nobody reads could take the whole integration
|
|
23
|
+
# down. That is exactly what happened in issue #18, where a Creator 5 reporting
|
|
24
|
+
# `chamberTemp: -108` made every entity unavailable and the config flow report
|
|
25
|
+
# `cannot_connect`.
|
|
26
|
+
#
|
|
27
|
+
# Range constraints belong on OUTBOUND command models (see responses.py), where
|
|
28
|
+
# the values are ours and a bad one is our bug. Inbound, we take what we need
|
|
29
|
+
# and tolerate the rest. This mirrors the TypeScript client (ff-5mp-api-ts),
|
|
30
|
+
# which has never hit this class of failure.
|
|
31
|
+
#
|
|
32
|
+
# Firmware signals "this sensor does not exist" with an out-of-band negative
|
|
33
|
+
# sentinel rather than by omitting the field: -108 on a chamber-less Creator 5,
|
|
34
|
+
# and -100 is our own "heater off" value (TempControl.TEMP_OFF). Anything at or
|
|
35
|
+
# below this floor is a marker, not a reading.
|
|
36
|
+
TEMP_SENTINEL_FLOOR = -50.0
|
|
37
|
+
|
|
38
|
+
_HEX_COLOR_RE = re.compile(r"^#[0-9A-Fa-f]{6}$")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def sanitize_temperature(value: Any) -> Any:
|
|
42
|
+
"""
|
|
43
|
+
Map firmware temperature sentinels to None, passing everything else through.
|
|
44
|
+
|
|
45
|
+
Non-numeric input is returned untouched so normal type validation still
|
|
46
|
+
reports it. Used as a `mode="before"` validator on every inbound
|
|
47
|
+
temperature field.
|
|
48
|
+
"""
|
|
49
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
50
|
+
return value
|
|
51
|
+
return None if value <= TEMP_SENTINEL_FLOOR else value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _normalize_hex_color(value: Any) -> Any:
|
|
55
|
+
"""
|
|
56
|
+
Coerce a material color into `#RRGGBB`, or an empty string when it is not
|
|
57
|
+
recognizable.
|
|
58
|
+
|
|
59
|
+
Never raises. An unparseable color costs a swatch its color; it must not
|
|
60
|
+
cost the caller the entire /detail response.
|
|
61
|
+
"""
|
|
62
|
+
if not isinstance(value, str):
|
|
63
|
+
return "" if value is None else value
|
|
64
|
+
|
|
65
|
+
candidate = value.strip()
|
|
66
|
+
if candidate == "" or _HEX_COLOR_RE.match(candidate):
|
|
67
|
+
return candidate
|
|
68
|
+
|
|
69
|
+
# Accept the shorthand `#RGB` form by expanding it, and a bare `RRGGBB`
|
|
70
|
+
# without the leading hash - both are plausible firmware variations.
|
|
71
|
+
if re.match(r"^#[0-9A-Fa-f]{3}$", candidate):
|
|
72
|
+
return "#" + "".join(char * 2 for char in candidate[1:])
|
|
73
|
+
if re.match(r"^[0-9A-Fa-f]{6}$", candidate):
|
|
74
|
+
return f"#{candidate}"
|
|
75
|
+
return ""
|
|
76
|
+
|
|
12
77
|
|
|
13
78
|
class MachineState(Enum):
|
|
14
79
|
"""Enumerates the possible operational states of the FlashForge 3D printer."""
|
|
@@ -35,12 +100,11 @@ class Temperature(BaseModel):
|
|
|
35
100
|
# `extra="allow"` would silently absorb.
|
|
36
101
|
model_config = ConfigDict(extra="forbid")
|
|
37
102
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
103
|
+
# No range constraints: MachineInfoParser feeds these straight from firmware
|
|
104
|
+
# values, so a range here fails `from_detail` for the same reason a range on
|
|
105
|
+
# FFPrinterDetail fails the response - one odd reading, no data at all.
|
|
106
|
+
current: float = Field(default=0.0, description="The current temperature in Celsius")
|
|
107
|
+
set: float = Field(default=0.0, description="The target (set) temperature in Celsius")
|
|
44
108
|
|
|
45
109
|
|
|
46
110
|
class SlotInfo(BaseModel):
|
|
@@ -53,26 +117,32 @@ class SlotInfo(BaseModel):
|
|
|
53
117
|
# integration reports as "could not retrieve printer information".
|
|
54
118
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
55
119
|
|
|
120
|
+
# Every field defaulted: a slot missing one attribute must cost that
|
|
121
|
+
# attribute, not the whole /detail response. `slot_id` carries no 1-4 range
|
|
122
|
+
# for the same reason - a station reporting a fifth slot should show up as a
|
|
123
|
+
# slot we ignore, not as an offline printer.
|
|
56
124
|
has_filament: bool = Field(
|
|
57
|
-
|
|
125
|
+
default=False,
|
|
126
|
+
alias="hasFilament",
|
|
127
|
+
description="Indicates if filament is present in this slot",
|
|
58
128
|
)
|
|
59
129
|
material_color: str = Field(
|
|
60
|
-
|
|
130
|
+
default="",
|
|
131
|
+
alias="materialColor",
|
|
132
|
+
description="Color of the material in this slot (e.g., '#FFFFFF')",
|
|
61
133
|
)
|
|
62
134
|
material_name: str = Field(
|
|
63
|
-
|
|
135
|
+
default="",
|
|
136
|
+
alias="materialName",
|
|
137
|
+
description="Name of the material in this slot (e.g., 'PLA')",
|
|
64
138
|
)
|
|
65
|
-
slot_id: int = Field(alias="slotId",
|
|
139
|
+
slot_id: int = Field(default=0, alias="slotId", description="Identifier for this slot (1-4)")
|
|
66
140
|
|
|
67
|
-
@field_validator("material_color")
|
|
141
|
+
@field_validator("material_color", mode="before")
|
|
68
142
|
@classmethod
|
|
69
|
-
def
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if v == "" or re.match(r"^#[0-9A-Fa-f]{6}$", v):
|
|
74
|
-
return v
|
|
75
|
-
raise ValueError(f"Invalid hex color format: {v}")
|
|
143
|
+
def normalize_material_color(cls, v: Any) -> Any:
|
|
144
|
+
"""Normalize the material color, falling back to '' rather than raising."""
|
|
145
|
+
return _normalize_hex_color(v)
|
|
76
146
|
|
|
77
147
|
|
|
78
148
|
class MatlStationInfo(BaseModel):
|
|
@@ -81,27 +151,29 @@ class MatlStationInfo(BaseModel):
|
|
|
81
151
|
# `extra="allow"`: inbound, see SlotInfo.
|
|
82
152
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
83
153
|
|
|
154
|
+
# Defaulted and unconstrained throughout, per the inbound policy at the top
|
|
155
|
+
# of this module. `slot_infos` in particular had `min_length=1`, which made
|
|
156
|
+
# an explicitly empty `slotInfos: []` - a station with nothing loaded - fail
|
|
157
|
+
# the entire /detail response.
|
|
84
158
|
current_load_slot: int = Field(
|
|
85
|
-
alias="currentLoadSlot",
|
|
159
|
+
default=0, alias="currentLoadSlot", description="Currently loading slot ID (0 if none)"
|
|
86
160
|
)
|
|
87
161
|
current_slot: int = Field(
|
|
88
|
-
alias="currentSlot",
|
|
162
|
+
default=0, alias="currentSlot", description="Currently active/printing slot ID (0 if none)"
|
|
89
163
|
)
|
|
90
164
|
slot_cnt: int = Field(
|
|
91
|
-
alias="slotCnt",
|
|
165
|
+
default=0, alias="slotCnt", description="Total number of slots in the station"
|
|
92
166
|
)
|
|
93
167
|
slot_infos: list[SlotInfo] = Field(
|
|
94
|
-
min_length=1,
|
|
95
|
-
max_length=4,
|
|
96
168
|
default_factory=list,
|
|
97
169
|
alias="slotInfos",
|
|
98
170
|
description="Array of information for each slot",
|
|
99
171
|
)
|
|
100
172
|
state_action: int = Field(
|
|
101
|
-
alias="stateAction",
|
|
173
|
+
default=0, alias="stateAction", description="Current action state of the material station"
|
|
102
174
|
)
|
|
103
175
|
state_step: int = Field(
|
|
104
|
-
alias="stateStep",
|
|
176
|
+
default=0, alias="stateStep", description="Current step within the state action"
|
|
105
177
|
)
|
|
106
178
|
|
|
107
179
|
|
|
@@ -111,24 +183,24 @@ class IndepMatlInfo(BaseModel):
|
|
|
111
183
|
# `extra="allow"`: inbound, see SlotInfo.
|
|
112
184
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
113
185
|
|
|
114
|
-
material_color: str = Field(
|
|
186
|
+
material_color: str = Field(
|
|
187
|
+
default="", alias="materialColor", description="Color of the material"
|
|
188
|
+
)
|
|
115
189
|
material_name: str = Field(
|
|
116
|
-
|
|
190
|
+
default="",
|
|
191
|
+
alias="materialName",
|
|
192
|
+
description="Name of the material (can be '?' if unknown)",
|
|
117
193
|
)
|
|
118
|
-
state_action: int = Field(alias="stateAction",
|
|
194
|
+
state_action: int = Field(default=0, alias="stateAction", description="Current action state")
|
|
119
195
|
state_step: int = Field(
|
|
120
|
-
alias="stateStep",
|
|
196
|
+
default=0, alias="stateStep", description="Current step within the state action"
|
|
121
197
|
)
|
|
122
198
|
|
|
123
|
-
@field_validator("material_color")
|
|
199
|
+
@field_validator("material_color", mode="before")
|
|
124
200
|
@classmethod
|
|
125
|
-
def
|
|
126
|
-
"""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if v == "" or re.match(r"^#[0-9A-Fa-f]{6}$", v):
|
|
130
|
-
return v
|
|
131
|
-
raise ValueError(f"Invalid hex color format: {v}")
|
|
201
|
+
def normalize_material_color(cls, v: Any) -> Any:
|
|
202
|
+
"""Normalize the material color, falling back to '' rather than raising."""
|
|
203
|
+
return _normalize_hex_color(v)
|
|
132
204
|
|
|
133
205
|
|
|
134
206
|
class FFGcodeToolData(BaseModel):
|
|
@@ -140,33 +212,28 @@ class FFGcodeToolData(BaseModel):
|
|
|
140
212
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
141
213
|
|
|
142
214
|
filament_weight: float = Field(
|
|
215
|
+
default=0.0,
|
|
143
216
|
alias="filamentWeight",
|
|
144
|
-
ge=0,
|
|
145
217
|
description="Calculated filament weight for this tool/material in the print",
|
|
146
218
|
)
|
|
147
219
|
material_color: str = Field(
|
|
148
|
-
alias="materialColor", description="Material color hex string (e.g., '#FFFF00')"
|
|
220
|
+
default="", alias="materialColor", description="Material color hex string (e.g., '#FFFF00')"
|
|
149
221
|
)
|
|
150
222
|
material_name: str = Field(
|
|
151
|
-
alias="materialName", description="Name of the material (e.g., 'PLA')"
|
|
223
|
+
default="", alias="materialName", description="Name of the material (e.g., 'PLA')"
|
|
152
224
|
)
|
|
153
225
|
slot_id: int = Field(
|
|
226
|
+
default=0,
|
|
154
227
|
alias="slotId",
|
|
155
|
-
ge=0,
|
|
156
|
-
le=4,
|
|
157
228
|
description="Slot ID from the material station, if applicable (0 if not or direct)",
|
|
158
229
|
)
|
|
159
|
-
tool_id: int = Field(alias="toolId",
|
|
230
|
+
tool_id: int = Field(default=0, alias="toolId", description="Tool ID or extruder number (0-3)")
|
|
160
231
|
|
|
161
|
-
@field_validator("material_color")
|
|
232
|
+
@field_validator("material_color", mode="before")
|
|
162
233
|
@classmethod
|
|
163
|
-
def
|
|
164
|
-
"""
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if v == "" or re.match(r"^#[0-9A-Fa-f]{6}$", v):
|
|
168
|
-
return v
|
|
169
|
-
raise ValueError(f"Invalid hex color format: {v}")
|
|
234
|
+
def normalize_material_color(cls, v: Any) -> Any:
|
|
235
|
+
"""Normalize the material color, falling back to '' rather than raising."""
|
|
236
|
+
return _normalize_hex_color(v)
|
|
170
237
|
|
|
171
238
|
|
|
172
239
|
class FFGcodeFileEntry(BaseModel):
|
|
@@ -178,28 +245,27 @@ class FFGcodeFileEntry(BaseModel):
|
|
|
178
245
|
# filament weight, and the per-tool material data for EVERY file.
|
|
179
246
|
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
180
247
|
|
|
248
|
+
# `gcode_file_name` stays required - it is the one field without which the
|
|
249
|
+
# entry means nothing, so failing here is honest. Everything else is
|
|
250
|
+
# metadata and gets a default.
|
|
181
251
|
gcode_file_name: str = Field(
|
|
182
252
|
alias="gcodeFileName", description="The name of the G-code file (e.g., 'FISH_PLA.3mf')"
|
|
183
253
|
)
|
|
184
254
|
gcode_tool_cnt: int | None = Field(
|
|
185
255
|
default=None,
|
|
186
|
-
ge=0,
|
|
187
|
-
le=4,
|
|
188
256
|
alias="gcodeToolCnt",
|
|
189
257
|
description="Number of tools/materials used in this G-code file",
|
|
190
258
|
)
|
|
191
259
|
gcode_tool_datas: list[FFGcodeToolData] | None = Field(
|
|
192
260
|
default=None,
|
|
193
|
-
max_length=4,
|
|
194
261
|
alias="gcodeToolDatas",
|
|
195
262
|
description="Array of detailed information for each tool/material",
|
|
196
263
|
)
|
|
197
264
|
printing_time: int = Field(
|
|
198
|
-
alias="printingTime",
|
|
265
|
+
default=0, alias="printingTime", description="Estimated printing time in seconds"
|
|
199
266
|
)
|
|
200
267
|
total_filament_weight: float | None = Field(
|
|
201
268
|
default=None,
|
|
202
|
-
ge=0,
|
|
203
269
|
alias="totalFilamentWeight",
|
|
204
270
|
description="Total estimated filament weight for the print",
|
|
205
271
|
)
|
|
@@ -220,33 +286,29 @@ class FFPrinterDetail(BaseModel):
|
|
|
220
286
|
model_config = ConfigDict(extra="allow")
|
|
221
287
|
|
|
222
288
|
auto_shutdown: str | None = Field(default=None, alias="autoShutdown")
|
|
223
|
-
auto_shutdown_time: int | None = Field(default=None,
|
|
224
|
-
camera: int | None = Field(default=None,
|
|
289
|
+
auto_shutdown_time: int | None = Field(default=None, alias="autoShutdownTime")
|
|
290
|
+
camera: int | None = Field(default=None, alias="camera")
|
|
225
291
|
camera_stream_url: str | None = Field(default=None, alias="cameraStreamUrl")
|
|
226
|
-
chamber_fan_speed: int | None = Field(default=None,
|
|
227
|
-
chamber_target_temp: float | None = Field(
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
chamber_temp: float | None = Field(default=None, ge=-50, le=500, alias="chamberTemp")
|
|
292
|
+
chamber_fan_speed: int | None = Field(default=None, alias="chamberFanSpeed")
|
|
293
|
+
chamber_target_temp: float | None = Field(default=None, alias="chamberTargetTemp")
|
|
294
|
+
chamber_temp: float | None = Field(default=None, alias="chamberTemp")
|
|
231
295
|
clear_fan_status: str | None = Field(default=None, alias="clearFanStatus")
|
|
232
|
-
cooling_fan_speed: int | None = Field(default=None,
|
|
233
|
-
cooling_fan_left_speed: int | None = Field(
|
|
234
|
-
default=None, ge=0, le=100, alias="coolingFanLeftSpeed"
|
|
235
|
-
)
|
|
296
|
+
cooling_fan_speed: int | None = Field(default=None, alias="coolingFanSpeed")
|
|
297
|
+
cooling_fan_left_speed: int | None = Field(default=None, alias="coolingFanLeftSpeed")
|
|
236
298
|
coordinate: list[float] | None = Field(default=None, alias="coordinate")
|
|
237
|
-
cumulative_filament: float | None = Field(default=None,
|
|
238
|
-
cumulative_print_time: int | None = Field(default=None,
|
|
239
|
-
current_print_speed: int | None = Field(default=None,
|
|
299
|
+
cumulative_filament: float | None = Field(default=None, alias="cumulativeFilament")
|
|
300
|
+
cumulative_print_time: int | None = Field(default=None, alias="cumulativePrintTime")
|
|
301
|
+
current_print_speed: int | None = Field(default=None, alias="currentPrintSpeed")
|
|
240
302
|
door_status: str | None = Field(default=None, alias="doorStatus")
|
|
241
303
|
error_code: str | None = Field(default=None, alias="errorCode")
|
|
242
|
-
estimated_left_len: float | None = Field(default=None,
|
|
243
|
-
estimated_left_weight: float | None = Field(default=None,
|
|
244
|
-
estimated_right_len: float | None = Field(default=None,
|
|
245
|
-
estimated_right_weight: float | None = Field(default=None,
|
|
246
|
-
estimated_time: float | None = Field(default=None,
|
|
247
|
-
extrude_ctrl: int | None = Field(default=None,
|
|
304
|
+
estimated_left_len: float | None = Field(default=None, alias="estimatedLeftLen")
|
|
305
|
+
estimated_left_weight: float | None = Field(default=None, alias="estimatedLeftWeight")
|
|
306
|
+
estimated_right_len: float | None = Field(default=None, alias="estimatedRightLen")
|
|
307
|
+
estimated_right_weight: float | None = Field(default=None, alias="estimatedRightWeight")
|
|
308
|
+
estimated_time: float | None = Field(default=None, alias="estimatedTime")
|
|
309
|
+
extrude_ctrl: int | None = Field(default=None, alias="extrudeCtrl")
|
|
248
310
|
external_fan_status: str | None = Field(default=None, alias="externalFanStatus")
|
|
249
|
-
fill_amount: float | None = Field(default=None,
|
|
311
|
+
fill_amount: float | None = Field(default=None, alias="fillAmount")
|
|
250
312
|
firmware_version: str | None = Field(default=None, alias="firmwareVersion")
|
|
251
313
|
flash_register_code: str | None = Field(default=None, alias="flashRegisterCode")
|
|
252
314
|
# AD5X-only, and firmware omits what does not apply: the Creator 5 series
|
|
@@ -261,48 +323,78 @@ class FFPrinterDetail(BaseModel):
|
|
|
261
323
|
internal_fan_status: str | None = Field(default=None, alias="internalFanStatus")
|
|
262
324
|
ip_addr: str | None = Field(default=None, alias="ipAddr")
|
|
263
325
|
left_filament_type: str | None = Field(default=None, alias="leftFilamentType")
|
|
264
|
-
left_target_temp: float | None = Field(default=None,
|
|
265
|
-
left_temp: float | None = Field(default=None,
|
|
326
|
+
left_target_temp: float | None = Field(default=None, alias="leftTargetTemp")
|
|
327
|
+
left_temp: float | None = Field(default=None, alias="leftTemp")
|
|
266
328
|
light_status: str | None = Field(default=None, alias="lightStatus")
|
|
267
329
|
location: str | None = Field(default=None, alias="location")
|
|
268
330
|
mac_addr: str | None = Field(default=None, alias="macAddr")
|
|
269
331
|
measure: str | None = Field(default=None, alias="measure")
|
|
270
|
-
move_ctrl: int | None = Field(default=None,
|
|
332
|
+
move_ctrl: int | None = Field(default=None, alias="moveCtrl")
|
|
271
333
|
name: str | None = Field(default=None, alias="name")
|
|
272
|
-
nozzle_cnt: int | None = Field(default=None,
|
|
334
|
+
nozzle_cnt: int | None = Field(default=None, alias="nozzleCnt")
|
|
273
335
|
nozzle_model: str | None = Field(default=None, alias="nozzleModel")
|
|
274
|
-
nozzle_style: int | None = Field(default=None,
|
|
336
|
+
nozzle_style: int | None = Field(default=None, alias="nozzleStyle")
|
|
275
337
|
# --- Creator 5 series raw fields ---
|
|
276
338
|
# Immutable factory model name (e.g. "Creator 5 Pro"); unlike `name` this is
|
|
277
339
|
# not user-editable. May be absent on older firmware.
|
|
278
340
|
model: str | None = Field(default=None, alias="model")
|
|
279
341
|
# Per-tool current nozzle temperatures (one entry per nozzle). Multi-nozzle
|
|
280
342
|
# Creator 5 series report these; single-nozzle models use rightTemp/leftTemp.
|
|
281
|
-
|
|
343
|
+
# Entries are nullable because a sentinel reading is normalized to None by
|
|
344
|
+
# `drop_nozzle_temperature_sentinels` below.
|
|
345
|
+
nozzle_temps: list[float | None] | None = Field(default=None, alias="nozzleTemps")
|
|
282
346
|
# Per-tool target nozzle temperatures (one entry per nozzle).
|
|
283
|
-
nozzle_target_temps: list[float] | None = Field(
|
|
347
|
+
nozzle_target_temps: list[float | None] | None = Field(
|
|
348
|
+
default=None, alias="nozzleTargetTemps"
|
|
349
|
+
)
|
|
284
350
|
# Lidar / first-layer scanner presence flag (1 = present, 0 = absent).
|
|
285
|
-
lidar: int | None = Field(default=None,
|
|
286
|
-
pid: int | None = Field(default=None,
|
|
287
|
-
plat_target_temp: float | None = Field(default=None,
|
|
288
|
-
plat_temp: float | None = Field(default=None,
|
|
351
|
+
lidar: int | None = Field(default=None, alias="lidar")
|
|
352
|
+
pid: int | None = Field(default=None, alias="pid")
|
|
353
|
+
plat_target_temp: float | None = Field(default=None, alias="platTargetTemp")
|
|
354
|
+
plat_temp: float | None = Field(default=None, alias="platTemp")
|
|
289
355
|
polar_register_code: str | None = Field(default=None, alias="polarRegisterCode")
|
|
290
|
-
print_duration: int | None = Field(default=None,
|
|
356
|
+
print_duration: int | None = Field(default=None, alias="printDuration")
|
|
291
357
|
print_file_name: str | None = Field(default=None, alias="printFileName")
|
|
292
358
|
print_file_thumb_url: str | None = Field(default=None, alias="printFileThumbUrl")
|
|
293
|
-
print_layer: int | None = Field(default=None,
|
|
294
|
-
print_progress: float | None = Field(default=None,
|
|
295
|
-
print_speed_adjust: int | None = Field(default=None,
|
|
296
|
-
remaining_disk_space: float | None = Field(default=None,
|
|
359
|
+
print_layer: int | None = Field(default=None, alias="printLayer")
|
|
360
|
+
print_progress: float | None = Field(default=None, alias="printProgress")
|
|
361
|
+
print_speed_adjust: int | None = Field(default=None, alias="printSpeedAdjust")
|
|
362
|
+
remaining_disk_space: float | None = Field(default=None, alias="remainingDiskSpace")
|
|
297
363
|
right_filament_type: str | None = Field(default=None, alias="rightFilamentType")
|
|
298
|
-
right_target_temp: float | None = Field(default=None,
|
|
299
|
-
right_temp: float | None = Field(default=None,
|
|
364
|
+
right_target_temp: float | None = Field(default=None, alias="rightTargetTemp")
|
|
365
|
+
right_temp: float | None = Field(default=None, alias="rightTemp")
|
|
300
366
|
status: str | None = Field(default=None, alias="status")
|
|
301
|
-
target_print_layer: int | None = Field(default=None,
|
|
302
|
-
tvoc: float | None = Field(default=None,
|
|
303
|
-
z_axis_compensation: float | None = Field(
|
|
304
|
-
|
|
367
|
+
target_print_layer: int | None = Field(default=None, alias="targetPrintLayer")
|
|
368
|
+
tvoc: float | None = Field(default=None, alias="tvoc")
|
|
369
|
+
z_axis_compensation: float | None = Field(default=None, alias="zAxisCompensation")
|
|
370
|
+
|
|
371
|
+
# Firmware reports absent temperature hardware with a negative sentinel
|
|
372
|
+
# (-108 on a chamber-less Creator 5, issue #18) rather than by omitting the
|
|
373
|
+
# field. Map those to None here so "no sensor" reads as "not reported"
|
|
374
|
+
# everywhere downstream, instead of as a -108 C reading or a hard failure.
|
|
375
|
+
@field_validator(
|
|
376
|
+
"chamber_temp",
|
|
377
|
+
"chamber_target_temp",
|
|
378
|
+
"left_temp",
|
|
379
|
+
"left_target_temp",
|
|
380
|
+
"plat_temp",
|
|
381
|
+
"plat_target_temp",
|
|
382
|
+
"right_temp",
|
|
383
|
+
"right_target_temp",
|
|
384
|
+
mode="before",
|
|
305
385
|
)
|
|
386
|
+
@classmethod
|
|
387
|
+
def drop_temperature_sentinels(cls, v: Any) -> Any:
|
|
388
|
+
"""Replace out-of-band 'no sensor' temperature markers with None."""
|
|
389
|
+
return sanitize_temperature(v)
|
|
390
|
+
|
|
391
|
+
@field_validator("nozzle_temps", "nozzle_target_temps", mode="before")
|
|
392
|
+
@classmethod
|
|
393
|
+
def drop_nozzle_temperature_sentinels(cls, v: Any) -> Any:
|
|
394
|
+
"""Apply the same sentinel handling to the per-tool temperature arrays."""
|
|
395
|
+
if not isinstance(v, list):
|
|
396
|
+
return v
|
|
397
|
+
return [sanitize_temperature(entry) for entry in v]
|
|
306
398
|
|
|
307
399
|
|
|
308
400
|
class FFMachineInfo(BaseModel):
|
|
@@ -393,6 +485,11 @@ class FFMachineInfo(BaseModel):
|
|
|
393
485
|
# Only true on models with confirmed hardware (Creator 5 Pro). When false,
|
|
394
486
|
# `door_open` is cosmetic and should not be surfaced.
|
|
395
487
|
has_door_sensor: bool = False
|
|
488
|
+
# True only when the printer actually reported a chamber temperature. The
|
|
489
|
+
# heated chamber is a Creator 5 series *option*, not a family trait: units
|
|
490
|
+
# without it report the -108 sentinel, which FFPrinterDetail normalizes to
|
|
491
|
+
# None. Gate chamber entities on this, never on `is_creator5`.
|
|
492
|
+
has_chamber_sensor: bool = False
|
|
396
493
|
|
|
397
494
|
# Current print stats
|
|
398
495
|
print_duration: int = 0
|
|
@@ -4,10 +4,13 @@ FlashForge Python API - Endstop Status Parser
|
|
|
4
4
|
Parses endstop and machine status information from M119 command responses.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import logging
|
|
7
8
|
import re
|
|
8
9
|
from enum import Enum
|
|
9
10
|
from typing import Optional
|
|
10
11
|
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
11
14
|
# Pre-compiled regex to match key:value pairs where value is an integer
|
|
12
15
|
KV_PATTERN = re.compile(r"([A-Za-z0-9-]+):\s*(\d+)")
|
|
13
16
|
|
|
@@ -144,7 +147,7 @@ class EndstopStatus:
|
|
|
144
147
|
elif "BUSY" in machine_status:
|
|
145
148
|
self.machine_status = MachineStatus.BUSY
|
|
146
149
|
else:
|
|
147
|
-
|
|
150
|
+
logger.warning("EndstopStatus: unknown MachineStatus: %s", machine_status)
|
|
148
151
|
self.machine_status = MachineStatus.DEFAULT
|
|
149
152
|
elif line.startswith("MoveMode:"):
|
|
150
153
|
move_mode = line.replace("MoveMode:", "", 1).strip().upper()
|
|
@@ -159,7 +162,7 @@ class EndstopStatus:
|
|
|
159
162
|
elif "HOMING" in move_mode:
|
|
160
163
|
self.move_mode = MoveMode.HOMING
|
|
161
164
|
else:
|
|
162
|
-
|
|
165
|
+
logger.warning("EndstopStatus: unknown MoveMode: %s", move_mode)
|
|
163
166
|
self.move_mode = MoveMode.DEFAULT
|
|
164
167
|
elif line.startswith("Status "):
|
|
165
168
|
self.status = Status(line)
|
|
@@ -184,9 +187,8 @@ class EndstopStatus:
|
|
|
184
187
|
return self
|
|
185
188
|
|
|
186
189
|
except Exception as e:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
print(f"Error: {e}")
|
|
190
|
+
logger.warning("Unable to create an EndstopStatus from the reply: %s", e)
|
|
191
|
+
logger.debug("Reply that failed to parse: %s", replay)
|
|
190
192
|
return None
|
|
191
193
|
|
|
192
194
|
def is_print_complete(self) -> bool:
|
|
@@ -4,8 +4,11 @@ FlashForge Python API - Print Status Parser
|
|
|
4
4
|
Parses print progress information from M27 command responses.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import logging
|
|
7
8
|
from typing import Optional
|
|
8
9
|
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
class PrintStatus:
|
|
11
14
|
"""
|
|
@@ -69,13 +72,13 @@ class PrintStatus:
|
|
|
69
72
|
return None
|
|
70
73
|
|
|
71
74
|
if not self.sd_current or not self.sd_total:
|
|
72
|
-
|
|
75
|
+
logger.warning("PrintStatus: invalid SD progress format.")
|
|
73
76
|
return None
|
|
74
77
|
|
|
75
78
|
return self
|
|
76
79
|
|
|
77
80
|
except Exception as e:
|
|
78
|
-
|
|
81
|
+
logger.warning("Error parsing the print status: %s", e)
|
|
79
82
|
return None
|
|
80
83
|
|
|
81
84
|
def get_print_percent(self) -> float:
|