flashforge-python-api 1.3.3__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/info.py +106 -19
- flashforge/exceptions.py +51 -0
- flashforge/models/machine_info.py +196 -99
- {flashforge_python_api-1.3.3.dist-info → flashforge_python_api-1.3.4.dist-info}/METADATA +1 -1
- {flashforge_python_api-1.3.3.dist-info → flashforge_python_api-1.3.4.dist-info}/RECORD +9 -8
- {flashforge_python_api-1.3.3.dist-info → flashforge_python_api-1.3.4.dist-info}/WHEEL +0 -0
- {flashforge_python_api-1.3.3.dist-info → flashforge_python_api-1.3.4.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.3.3.dist-info → flashforge_python_api-1.3.4.dist-info}/licenses/LICENSE +0 -0
flashforge/__init__.py
CHANGED
|
@@ -69,6 +69,7 @@ from .discovery import (
|
|
|
69
69
|
PrinterStatus,
|
|
70
70
|
SocketCreationError,
|
|
71
71
|
)
|
|
72
|
+
from .exceptions import FlashForgeError, FlashForgeResponseError
|
|
72
73
|
|
|
73
74
|
# Import key models for convenience
|
|
74
75
|
from .models import (
|
|
@@ -127,7 +128,7 @@ from .tcp import (
|
|
|
127
128
|
)
|
|
128
129
|
|
|
129
130
|
FiveMClient = FlashForgeClient
|
|
130
|
-
__version__ = "1.3.
|
|
131
|
+
__version__ = "1.3.4"
|
|
131
132
|
__author__ = "FlashForge Python API Contributors"
|
|
132
133
|
__email__ = "notghosttypes@gmail.com"
|
|
133
134
|
__description__ = "Python library for controlling FlashForge 3D printers"
|
|
@@ -138,6 +139,9 @@ __all__ = [
|
|
|
138
139
|
"FlashForgeClient",
|
|
139
140
|
"FiveMClient",
|
|
140
141
|
"FiveMClientConnectionOptions",
|
|
142
|
+
# Exceptions
|
|
143
|
+
"FlashForgeError",
|
|
144
|
+
"FlashForgeResponseError",
|
|
141
145
|
# Data models
|
|
142
146
|
"FFMachineInfo",
|
|
143
147
|
"FFPrinterDetail",
|
flashforge/api/controls/info.py
CHANGED
|
@@ -5,8 +5,9 @@ FlashForge Python API - Info Module
|
|
|
5
5
|
import logging
|
|
6
6
|
import re
|
|
7
7
|
from datetime import datetime, timedelta
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
9
|
|
|
10
|
+
from ...exceptions import FlashForgeResponseError
|
|
10
11
|
from ...models.machine_info import FFMachineInfo, MachineState, Temperature
|
|
11
12
|
from ...models.responses import DetailResponse, FFPrinterDetail
|
|
12
13
|
from ..constants.endpoints import Endpoints
|
|
@@ -165,6 +166,12 @@ class MachineInfoParser:
|
|
|
165
166
|
)
|
|
166
167
|
has_lidar = getattr(detail, "lidar", None) == 1
|
|
167
168
|
has_door_sensor = is_creator5_pro
|
|
169
|
+
# Not every Creator 5 has the heated chamber. FFPrinterDetail maps
|
|
170
|
+
# the firmware's "no sensor" sentinel (-108) to None, so an absent
|
|
171
|
+
# reading is the signal - never the model family, which is what left
|
|
172
|
+
# chamber-less units with three entities pinned at 0 C.
|
|
173
|
+
chamber_temp_value = getattr(detail, "chamber_temp", None)
|
|
174
|
+
has_chamber_sensor = chamber_temp_value is not None
|
|
168
175
|
|
|
169
176
|
# Immutable model name resolution: prefer the firmware `model`
|
|
170
177
|
# field, then a PID-derived name, then the user-set name.
|
|
@@ -237,6 +244,7 @@ class MachineInfoParser:
|
|
|
237
244
|
has_camera=has_camera,
|
|
238
245
|
has_lidar=has_lidar,
|
|
239
246
|
has_door_sensor=has_door_sensor,
|
|
247
|
+
has_chamber_sensor=has_chamber_sensor,
|
|
240
248
|
# Current print stats
|
|
241
249
|
print_duration=getattr(detail, "print_duration", 0) or 0,
|
|
242
250
|
print_file_name=getattr(detail, "print_file_name", "") or "",
|
|
@@ -338,21 +346,41 @@ class Info:
|
|
|
338
346
|
This method fetches detailed data from the printer and transforms it.
|
|
339
347
|
|
|
340
348
|
Returns:
|
|
341
|
-
An FFMachineInfo object, or None if
|
|
349
|
+
An FFMachineInfo object, or None if the printer could not be reached.
|
|
350
|
+
|
|
351
|
+
Raises:
|
|
352
|
+
FlashForgeResponseError: The printer answered but the payload could
|
|
353
|
+
not be understood. Distinct from a None return, which means the
|
|
354
|
+
request never got through.
|
|
342
355
|
"""
|
|
343
356
|
detail_response = await self.get_detail_response()
|
|
344
|
-
if detail_response
|
|
345
|
-
return
|
|
346
|
-
|
|
357
|
+
if detail_response is None or detail_response.detail is None:
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
machine_info = MachineInfoParser.from_detail(detail_response.detail)
|
|
361
|
+
if machine_info is None:
|
|
362
|
+
# We validated the payload but could not build our own model from
|
|
363
|
+
# it - that is a bug on our side, not an unreachable printer, and
|
|
364
|
+
# reporting it as the latter is what issue #18 was about.
|
|
365
|
+
raise FlashForgeResponseError(
|
|
366
|
+
"The /detail payload was read but could not be converted into machine info",
|
|
367
|
+
endpoint=Endpoints.DETAIL,
|
|
368
|
+
)
|
|
369
|
+
return machine_info
|
|
347
370
|
|
|
348
371
|
async def is_printing(self) -> bool:
|
|
349
372
|
"""
|
|
350
373
|
Checks if the printer is currently in the "printing" state.
|
|
351
374
|
|
|
375
|
+
These three convenience wrappers keep their documented "return the
|
|
376
|
+
fallback on any failure" contract, so a caller polling for a boolean is
|
|
377
|
+
not forced to handle an exception. The failure is still logged, and
|
|
378
|
+
callers that need to distinguish causes should use `get()` directly.
|
|
379
|
+
|
|
352
380
|
Returns:
|
|
353
381
|
True if the printer is printing, False otherwise or if status cannot be determined.
|
|
354
382
|
"""
|
|
355
|
-
info = await self.
|
|
383
|
+
info = await self._get_or_none()
|
|
356
384
|
return info.status == "printing" if info else False
|
|
357
385
|
|
|
358
386
|
async def get_status(self) -> str | None:
|
|
@@ -362,7 +390,7 @@ class Info:
|
|
|
362
390
|
Returns:
|
|
363
391
|
The status string, or None if it cannot be determined.
|
|
364
392
|
"""
|
|
365
|
-
info = await self.
|
|
393
|
+
info = await self._get_or_none()
|
|
366
394
|
return info.status if info else None
|
|
367
395
|
|
|
368
396
|
async def get_machine_state(self) -> MachineState | None:
|
|
@@ -372,17 +400,34 @@ class Info:
|
|
|
372
400
|
Returns:
|
|
373
401
|
A MachineState enum value, or None if it cannot be determined.
|
|
374
402
|
"""
|
|
375
|
-
info = await self.
|
|
403
|
+
info = await self._get_or_none()
|
|
376
404
|
return info.machine_state if info else None
|
|
377
405
|
|
|
378
|
-
async def
|
|
406
|
+
async def _get_or_none(self) -> FFMachineInfo | None:
|
|
407
|
+
"""Call `get()`, absorbing a response error into a None for the convenience wrappers."""
|
|
408
|
+
try:
|
|
409
|
+
return await self.get()
|
|
410
|
+
except FlashForgeResponseError as error:
|
|
411
|
+
logger.warning("Could not read machine info: %s", error)
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
async def get_detail_raw(self) -> dict[str, Any] | None:
|
|
379
415
|
"""
|
|
380
|
-
Retrieves the
|
|
381
|
-
|
|
416
|
+
Retrieves the /detail response as the plain decoded JSON dict, with no
|
|
417
|
+
model validation applied.
|
|
418
|
+
|
|
419
|
+
This exists so callers can inspect identity fields - above all `pid` -
|
|
420
|
+
without first having to successfully validate the ~50 unrelated fields
|
|
421
|
+
in the payload. A supported printer must never be rejected because of a
|
|
422
|
+
field that has nothing to do with whether it is supported (issue #18).
|
|
382
423
|
|
|
383
424
|
Returns:
|
|
384
|
-
|
|
385
|
-
or
|
|
425
|
+
The decoded response body, or None if the printer could not be
|
|
426
|
+
reached or answered with a non-200 status.
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
FlashForgeResponseError: The printer answered with a body that is
|
|
430
|
+
not decodable JSON.
|
|
386
431
|
"""
|
|
387
432
|
payload = {"serialNumber": self.client.serial_number, "checkCode": self.client.check_code}
|
|
388
433
|
|
|
@@ -396,15 +441,57 @@ class Info:
|
|
|
396
441
|
if response.status != 200:
|
|
397
442
|
logger.warning("Non-200 status from the /detail endpoint: %s", response.status)
|
|
398
443
|
return None
|
|
399
|
-
|
|
400
444
|
data = await json_from_response(response)
|
|
401
|
-
return DetailResponse(**data)
|
|
402
|
-
|
|
403
445
|
except Exception as error:
|
|
404
446
|
logger.warning(
|
|
405
|
-
"Could not
|
|
406
|
-
"
|
|
407
|
-
"the
|
|
447
|
+
"Could not reach the printer's /detail endpoint. This is a transport "
|
|
448
|
+
"failure (unreachable printer, timeout, or refused connection), not a "
|
|
449
|
+
"payload the library failed to read. %s",
|
|
408
450
|
error,
|
|
409
451
|
)
|
|
410
452
|
return None
|
|
453
|
+
|
|
454
|
+
if not isinstance(data, dict):
|
|
455
|
+
raise FlashForgeResponseError(
|
|
456
|
+
f"The /detail endpoint returned {type(data).__name__}, expected a JSON object",
|
|
457
|
+
endpoint=Endpoints.DETAIL,
|
|
458
|
+
)
|
|
459
|
+
return data
|
|
460
|
+
|
|
461
|
+
async def get_detail_response(self) -> DetailResponse | None:
|
|
462
|
+
"""
|
|
463
|
+
Retrieves the validated detailed response from the printer's detail endpoint.
|
|
464
|
+
This contains a wealth of information about the printer's current state.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
A DetailResponse object containing the raw printer details, or None
|
|
468
|
+
if the printer could not be reached.
|
|
469
|
+
|
|
470
|
+
Raises:
|
|
471
|
+
FlashForgeResponseError: The printer answered, but the body did not
|
|
472
|
+
validate. Callers should surface this differently from a None
|
|
473
|
+
return: None means "we never got an answer", this means "the
|
|
474
|
+
answer was not one we could read", and only the second is
|
|
475
|
+
actionable as a bug report.
|
|
476
|
+
"""
|
|
477
|
+
data = await self.get_detail_raw()
|
|
478
|
+
if data is None:
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
return DetailResponse(**data)
|
|
483
|
+
except Exception as error:
|
|
484
|
+
logger.warning(
|
|
485
|
+
"The printer answered /detail with a payload that failed validation. This "
|
|
486
|
+
"is NOT a connectivity problem - please report it with the debug dump "
|
|
487
|
+
"below. %s",
|
|
488
|
+
error,
|
|
489
|
+
)
|
|
490
|
+
# Redacted: this dump carries the MAC, IP and cloud registration
|
|
491
|
+
# codes, and it exists to be pasted into a bug report.
|
|
492
|
+
logger.debug("Payload that failed validation: %s", redact_model(data))
|
|
493
|
+
raise FlashForgeResponseError(
|
|
494
|
+
"The printer's /detail response could not be validated",
|
|
495
|
+
endpoint=Endpoints.DETAIL,
|
|
496
|
+
cause=error,
|
|
497
|
+
) from error
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashforge-python-api
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
4
4
|
Summary: A comprehensive Python library for controlling FlashForge 3D printers
|
|
5
5
|
Project-URL: Homepage, https://github.com/GhostTypes/ff-5mp-api-py
|
|
6
6
|
Project-URL: Documentation, https://github.com/GhostTypes/ff-5mp-api-py#readme
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
flashforge/__init__.py,sha256=
|
|
1
|
+
flashforge/__init__.py,sha256=pTj91BlyK6rFMy9yD03q8IowcSrUm9ra-b1vMCYEJDo,5472
|
|
2
2
|
flashforge/client.py,sha256=nDtnv3y9XlEQknIkgIUiuOYAiyTEGbQ7tHBdCs2fg1I,21432
|
|
3
|
+
flashforge/exceptions.py,sha256=rZwBJHyqqtoI6cgd8iGOvPH8EDcytA3lyU3FoVMJgyU,1732
|
|
3
4
|
flashforge/api/__init__.py,sha256=vQz-DkG6LTH39bI4fyiUc0D9jQzemLh45pORLptSOlg,335
|
|
4
5
|
flashforge/api/constants/__init__.py,sha256=Q0HL2tqSBYPd4Oz49VHLS3qUvRuv__GCvTGecaLrQ-Y,163
|
|
5
6
|
flashforge/api/constants/commands.py,sha256=XM2rooBESPDaywlZrfW2ECTaLLFaSKXkJ7FQ-RBmVdY,578
|
|
@@ -8,7 +9,7 @@ flashforge/api/controls/__init__.py,sha256=53s-H25Pjwr0kvPk8MZ2Twy8VHGqGcO6Q6uBp
|
|
|
8
9
|
flashforge/api/controls/control.py,sha256=OgE7K6NkFLwFN1r_q5g1RPwYC8MCt0f6L5CZxDW44d8,15151
|
|
9
10
|
flashforge/api/controls/creator5_palette.py,sha256=zdD3PIXx7hCVj2Fmv1CSFOaqogRUPeBaymgY2DFiMxk,8835
|
|
10
11
|
flashforge/api/controls/files.py,sha256=25yxln9NROas9TtFMZR9e8LtA8ruk8RUu2C81vpNgTk,8043
|
|
11
|
-
flashforge/api/controls/info.py,sha256=
|
|
12
|
+
flashforge/api/controls/info.py,sha256=AZ5529HbB1LBfae6voz_4mz30pQd8R8HpcRrPa5qXCw,22451
|
|
12
13
|
flashforge/api/controls/job_control.py,sha256=NnACm4LPUz4n8lnRiUl33jilWekS1b6HQTbP--nDzZc,33225
|
|
13
14
|
flashforge/api/controls/temp_control.py,sha256=EOO5TkODirjFcnQ7rZrirxAweHBdVin032AWj-Z8VQs,13040
|
|
14
15
|
flashforge/api/filament/__init__.py,sha256=isT2dl0hzUS0xMTXMm4Tip0GTG1gCsm8LKWa9xPu4Y8,104
|
|
@@ -23,7 +24,7 @@ flashforge/api/network/utils.py,sha256=Q5-Vj_1VN611QV_TGpDzrrZ2X8PqaGT0j00BhSHv5
|
|
|
23
24
|
flashforge/discovery/__init__.py,sha256=G0WiP70EhfHdjXR1RNlv6xjhyHOdo-HWyOm9J0ds4SM,828
|
|
24
25
|
flashforge/discovery/discovery.py,sha256=-hOmVIpn8gu-rPYo8FRBfhKb_tBqwamLfmoPVYaDzLI,27614
|
|
25
26
|
flashforge/models/__init__.py,sha256=hdcK0E4KeDfv_A8Y2aPOHgHziwTjP1RPb_VP-89dOzQ,1086
|
|
26
|
-
flashforge/models/machine_info.py,sha256=
|
|
27
|
+
flashforge/models/machine_info.py,sha256=o0rpT6DPWojg8VMfYleMFIqEaidDvsY6E0MstaVHOT0,24065
|
|
27
28
|
flashforge/models/responses.py,sha256=10-8yFfZtLuDmH1TK2xVYl-Uf-zT2WEJnpb2H5ZK3ZU,10637
|
|
28
29
|
flashforge/tcp/__init__.py,sha256=hpnqoWHeRtTwJPzdsVlwGt1njKbAkrgIHKADkLSIRec,1317
|
|
29
30
|
flashforge/tcp/a3_client.py,sha256=Aqb6MuKkjnNUVdE2LbZKd7bPlFK8XPIm17C_BG_PuNU,15867
|
|
@@ -41,8 +42,8 @@ flashforge/tcp/parsers/print_status.py,sha256=C-KzukhK0TR86AP3t3XrMH4dief6pa3I-y
|
|
|
41
42
|
flashforge/tcp/parsers/printer_info.py,sha256=CHPs6nJfEByXyG3co7Nb8_ygggxkYuc7A9O9bpQEqXE,5441
|
|
42
43
|
flashforge/tcp/parsers/temp_info.py,sha256=9wRGUM9cKvDiZD3SIxBx_qVLZbeXoYsoY3-R3vN3H4g,7938
|
|
43
44
|
flashforge/tcp/parsers/thumbnail_info.py,sha256=1U1S_gcIZ2lDpEA4622ywUew6TVw0BnzjlBAiihQwzk,10478
|
|
44
|
-
flashforge_python_api-1.3.
|
|
45
|
-
flashforge_python_api-1.3.
|
|
46
|
-
flashforge_python_api-1.3.
|
|
47
|
-
flashforge_python_api-1.3.
|
|
48
|
-
flashforge_python_api-1.3.
|
|
45
|
+
flashforge_python_api-1.3.4.dist-info/METADATA,sha256=Kdqkl6GJBRr3vRFf6p6Qk-qZ_8TtSUCNS0jdzEIxvHE,4970
|
|
46
|
+
flashforge_python_api-1.3.4.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
47
|
+
flashforge_python_api-1.3.4.dist-info/entry_points.txt,sha256=AkOxlsLvQ7cvMLxn7tlzfKp_DCH2hXhbVceHIXxawpU,66
|
|
48
|
+
flashforge_python_api-1.3.4.dist-info/licenses/LICENSE,sha256=-cTA-hrmvlb3pqlQrBZQXUnKayhUjsLJMPb7TD91frM,1067
|
|
49
|
+
flashforge_python_api-1.3.4.dist-info/RECORD,,
|
|
File without changes
|
{flashforge_python_api-1.3.3.dist-info → flashforge_python_api-1.3.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{flashforge_python_api-1.3.3.dist-info → flashforge_python_api-1.3.4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|