Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- **A paused Creator 5 Pro no longer reports as "unknown".** The printer sends `status: "pause"`, while the mapping only knew the documented `"pausing"` and `"paused"` — so every pause fell through to `MachineState.UNKNOWN`. In Home Assistant that is the one state that says nothing, and it appeared at exactly the wrong moment: the printer pauses itself when it detects a clog, so the sensor went blank precisely when the user needed to know why the print had stopped. Found in a Home Assistant log carrying 84 `Unknown machine status received: 'pause'` warnings over a single day (pid 41, firmware 1.9.4). `"pause"` now maps to `PAUSED`; `"paused"` still does, because firmware reporting one is no reason to drop the other.

- **`status: "downloading"` is mapped too.** Reported while a file is being transferred to the printer, and likewise unmapped until now. It maps to `BUSY` rather than to a new enum member on purpose: consumers may pin this enum to a fixed list — the Home Assistant integration's Machine Status sensor is a `device_class=ENUM` with an explicit `options` list — so adding a member breaks them until they ship a matching release, while an existing state does not. A dedicated `DOWNLOADING` state is worth discussing, but it is a coordinated change, not a bugfix.

## [1.3.4] - 2026-07-26

### Fixed
Expand Down
27 changes: 26 additions & 1 deletion flashforge/api/controls/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,21 @@ def _format_time_from_seconds(seconds: float) -> str:

@staticmethod
def _get_machine_state(status: str) -> MachineState:
"""Map raw status strings into the public machine state enum."""
"""Map raw status strings into the public machine state enum.

An unmapped value costs the consumer everything the field is for: it
becomes ``UNKNOWN``, which in Home Assistant renders as "unknown" - the
one state that says nothing, at the moment the user most needs to know
what the printer is doing. Both additions below were found that way, in
a log full of `Unknown machine status received` while a Creator 5 Pro sat
paused on a detected clog.

Consumers may map this enum onto a fixed set of values - the Home
Assistant integration's Machine Status sensor is a `device_class=ENUM`
with an explicit `options` list - so a *new* member is a breaking change
for them, while mapping onto an existing one is not. Prefer the closest
existing state unless a new one is worth coordinating.
"""
valid_status = status.lower() if isinstance(status, str) else ""
state_mapping = {
"ready": MachineState.READY,
Expand All @@ -312,9 +326,20 @@ def _get_machine_state(status: str) -> MachineState:
"heating": MachineState.HEATING,
"printing": MachineState.PRINTING,
"pausing": MachineState.PAUSING,
# The Creator 5 Pro reports "pause" for a print that is paused,
# where the documented value is "paused" - both are mapped, because
# firmware that reports one is not a reason to drop the other.
# Observed on pid 41, firmware 1.9.4, whenever the printer paused
# itself on a detected clog.
"pause": MachineState.PAUSED,
"paused": MachineState.PAUSED,
"cancel": MachineState.CANCELLED,
"completed": MachineState.COMPLETED,
# Reported while a file is being transferred to the printer. Not a
# print, but not idle either, so BUSY is the honest existing fit; a
# dedicated state would need consumers to add it to their option
# lists first.
"downloading": MachineState.BUSY,
}

if valid_status in state_mapping:
Expand Down
45 changes: 45 additions & 0 deletions tests/test_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,48 @@ async def test_convenience_wrappers_absorb_response_errors():
assert await info.is_printing() is False
assert await info.get_status() is None
assert await info.get_machine_state() is None


# --------------------------------------------------------------------------- #
# Status mapping
# --------------------------------------------------------------------------- #


@pytest.mark.parametrize(
("reported", "expected"),
[
("ready", MachineState.READY),
("printing", MachineState.PRINTING),
("pausing", MachineState.PAUSING),
("paused", MachineState.PAUSED),
# A Creator 5 Pro reports "pause", not the documented "paused".
("pause", MachineState.PAUSED),
("PAUSE", MachineState.PAUSED),
# Sent while a file is being transferred to the printer.
("downloading", MachineState.BUSY),
("completed", MachineState.COMPLETED),
],
)
def test_machine_state_mapping(reported, expected):
"""Every status a real printer has been seen to report must map."""
assert MachineInfoParser._get_machine_state(reported) is expected


def test_unmapped_status_is_unknown_and_logged(caplog):
"""An unrecognized status still degrades to UNKNOWN - and says so.

The warning is the only trace an unmapped value leaves: the state itself
becomes UNKNOWN, which is indistinguishable from a printer that reported
nothing. Both "pause" and "downloading" were found this way, in a Home
Assistant log, after the sensor had been reading "unknown" for a day.
"""
with caplog.at_level("WARNING"):
assert MachineInfoParser._get_machine_state("teleporting") is MachineState.UNKNOWN

assert "teleporting" in caplog.text


def test_empty_status_is_unknown_without_a_warning():
"""No status is not an unexpected status; it must not cry wolf."""
assert MachineInfoParser._get_machine_state("") is MachineState.UNKNOWN
assert MachineInfoParser._get_machine_state(None) is MachineState.UNKNOWN
Loading