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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ Thumbs.db
*.log
homeassistant/*
/homeassistant-prod

# Local codegraph index (tooling artifact)
.codegraph/
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Working agreement for AI assistants contributing to `ff-5mp-hass`.
- **Integration:** FlashForge printers for Home Assistant (HTTP API only).
- **Current release:** `v1.3.0` (in-flight; not yet tagged). Last published: `v1.2.0`.
- **Supported printers:** `AD5X`, `Adventurer 5M`, `Adventurer 5M Pro`, `Creator 5`, and `Creator 5 Pro` only.
- **Entities shipped:** 56 total (38 sensors, 5 binary sensors, 2 switches, 1 select, 4 buttons, 1 camera, 5 images — the g-code thumbnail plus 4 Material Station slot color swatches).
- **Entities shipped:** 56 total (38 sensors, 5 binary sensors, 2 switches (the camera switch is not created on the Creator 5 series), 1 select, 4 buttons, 1 camera, 5 images — the g-code thumbnail plus 4 Material Station slot color swatches).
- **Key dependency:** `flashforge-python-api>=1.3.0` (see sibling repo `ff-5mp-api-py`).
- **Primary references:** `CLAUDE.md` (agent workflow), `AGENTS.md` (this playbook), `README.md` (user docs), `CHANGELOG.md`, `homeassistant/README.md`, `HOME_ASSISTANT_DOCS_COMPANION.md`, and `HACS_PUBLISHER_COMPANION.md`.

Expand Down Expand Up @@ -39,6 +39,10 @@ Working agreement for AI assistants contributing to `ff-5mp-hass`.
- Record any manual testing nuances in pull requests, issues, or release notes for future reference.
- **Identify printers by PID, not name.** `config_flow.py` `_is_supported_detail()` checks the firmware-set `pid` on `/detail` against `SUPPORTED_PIDS = {35, 36, 38, 40, 41}` (5M, 5M Pro, AD5X, Creator 5, Creator 5 Pro); the upstream library (≥1.3.0) does the same internally to derive `is_pro` / `is_ad5x` / `is_creator5` / `is_creator5_pro`. The `name` field is user-mutable via the LCD or cloud and must never be substring-matched for model detection (broke in v1.1.8, fixed in v1.1.9 — see issue #13). Adding a new modern PID means updating `SUPPORTED_PIDS` here AND bumping the library dep floor.
- **Never trust the `/product` endpoint for capability gating.** It reliably reports filtration control for the Adventurer 5M Pro but misreports it for the Creator 5 Pro (wrong values). TVOC, chamber fan speed, and the filtration select are therefore gated on model identity (`is_pro OR is_creator5_pro`), not on the `/product`-derived `client.filtration_control` flag. Once the API gates capabilities internally we can re-derive from `/product`.
- **A capability that can be "unknown" will be read as "no".** Firmware omits fields that don't apply to a model, so an absent value means "not reported", and any `None`-able flag invites a consumer to collapse the two. `hasMatlStation` is the case that bit us: AD5X-only, absent on a Creator 5 Pro that has four loaded slots, so the Material Station entities never appeared on the models that have a station. Gate on a derived, always-concrete capability — `FFMachineInfo.has_matl_station` (library ≥1.3.2) — never on a raw `/detail` passthrough. The same trap caught `led_control_override`, where the option's unset `False` was read by the library as "force the capability off" and greyed out the LED switch on every printer; tri-state parameters need `None` for "no opinion".
- **Capability-gated entities must be added when the capability appears, not only at setup.** Platform setup can run before the printer has reported a capability, and the first refresh may fail outright. Add what is available, then watch `coordinator.async_add_listener` for the rest (see `image.py` / `sensor.py`), latching so a capability is only added once and registering the teardown with `entry.async_on_unload`.
- **A button's availability must not depend on anything but reachability.** A button entity is stateless — its state *is* the last-press timestamp — so any write of that state is reported to the logbook as a press. Gating availability on a selection or a mode means changing that input logs a phantom press. Validate in `async_press` and raise `ServiceValidationError` instead.
- **"Unavailable" and "not created" are different answers; pick the one that is true.** Grey an entity out (`availability_fn`) when the printer *could* report the feature later — that is a temporary state. Omit it entirely (`supported_fn` in `switch.py`, applied once at setup where model identity is already known) when the model's API cannot perform the action at all. The Creator 5 camera switch is the case: its `streamCtrl_cmd` returns success and does nothing, and `cameraStreamUrl` stays populated so the switch snaps back to `on` — a control that accepts a press, reports success and changes nothing is worse than a missing one. Confirm on hardware before deciding a command is inert; "available but unconfirmed" is a fine interim state, "available and known-inert" is not.

## Standard Workflows
### Feature or Bug Fix
Expand Down
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ 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]
## [1.3.2] - 2026-07-26

### Changed

- **The camera switch is no longer created on the Creator 5 series.** v1.3.1 made `switch.<printer>_camera` *available* on those models, but hardware testing on a Creator 5 Pro (firmware 1.9.4) confirmed the API can no longer act on it: `streamCtrl_cmd` with `action: close` answers `{"code": 0, "Success"}` while port 8080 keeps serving live MJPEG frames 22 seconds later, and `cameraStreamUrl` never changes. The switch could not reflect an off state either — `is_on` reads that same always-present URL, so the next poll flipped it straight back to `on`. Rather than ship a control that silently does nothing, the switch is now omitted entirely on the Creator 5 / Creator 5 Pro; the `camera` entity itself is unaffected and continues to stream. Refs [#17](https://github.com/GhostTypes/ff-5mp-hass/issues/17).

### Fixed

- **Material Station entities now appear on the Creator 5 series.** The four slot swatches (`image.<printer>_ifs_slot_1..4`) and the Active Material Station Slot sensor were gated on `FFMachineInfo.has_matl_station`, which was a straight copy of the raw `hasMatlStation` field from `/detail`. A Creator 5 Pro does not report that field at all — verified on real hardware (pid 41, firmware 1.9.4), where it is absent from `/detail` entirely while `matlStationInfo` reports `slotCnt: 4` and four loaded slots. The flag parsed as `None`, the entities were never created, and the v1.3.0 change that moved the gate off `is_ad5x` had no effect on this model. Fixed in the library (`flashforge-python-api` 1.3.2), which now derives the capability from the slot data; the minimum requirement is raised accordingly.
- **Capability-gated entities are no longer decided once at setup.** The Material Station slot images and every `availability_fn`-gated sensor are now also added when their capability first shows up on a later refresh. Platform setup can run before the printer has reported a capability, and the first refresh may fail outright — either case previously left the printer permanently without those entities.

- **The LED switch is no longer greyed out on every printer.** The "Always show LED switch" option was passed to the library as `led_control_override` using its unset value `False`. That parameter is tri-state — `None` means "no override", `True` forces the capability on, and `False` forces it **off** — so with the option switched off, which is the default, the integration overrode the printer's own correct capability report and pinned `client.led_control` to `False` on every model. The switch stayed unavailable, and the library additionally refused `set_led_on()` / `set_led_off()` internally, which made enabling the override look like the only way to get a working switch: `True` was the only value that got past the veto. The option now sends `None` when off and `True` only when the user asks for it, which is what it was always meant to do. Reported and diagnosed on a Creator 5 Pro, where `/product` correctly reports `lightCtrlState: 1` all along. Refs [#17](https://github.com/GhostTypes/ff-5mp-hass/issues/17).

## [1.3.1] - 2026-07-23

Expand Down Expand Up @@ -212,7 +223,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- FlashForge Adventurer 5M Series
- FlashForge Adventurer 4

[Unreleased]: https://github.com/GhostTypes/ff-5mp-hass/compare/v1.3.1...HEAD
[Unreleased]: https://github.com/GhostTypes/ff-5mp-hass/compare/v1.3.2...HEAD
[1.3.2]: https://github.com/GhostTypes/ff-5mp-hass/compare/v1.3.1...v1.3.2
[1.3.1]: https://github.com/GhostTypes/ff-5mp-hass/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/GhostTypes/ff-5mp-hass/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/GhostTypes/ff-5mp-hass/compare/v1.1.9...v1.2.0
Expand Down
32 changes: 16 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

Guidance for AI coding assistants working in this repository.

## Current State (May 2026)
- Integration **version 1.3.0** (in-flight; not yet tagged).
## Current State (July 2026)
- Integration **version 1.3.2** (in-flight; 1.3.1 tagged 2026-07-23).
- Provides a complete Home Assistant experience for FlashForge printers using the **HTTP API only**.
- Entities shipped: **56 total** (38 sensors, 5 binary sensors, 2 switches, 4 buttons, 1 select, 1 MJPEG camera, 5 images — the g-code thumbnail plus 4 Material Station slot color swatches).
- Diagnostics download supported (`diagnostics.py`), with credentials and identifiers redacted.
Expand Down Expand Up @@ -58,7 +58,7 @@ Treat this file as the living source of truth for workflows and expectations—u
- Entities grouped under a single device with manufacturer/model metadata.
- **Control**
- LED switch with capability detection (graceful "unavailable" for unsupported models, with an option to override the check).
- Filtration as a `select` entity with Off / Internal / External states (AD5X only).
- Filtration as a `select` entity with Off / Internal / External states (Adventurer 5M Pro / Creator 5 Pro only — gated on `is_pro OR is_creator5_pro`).
- Pause / resume / cancel / clear-status buttons with post-action refresh.
- MJPEG camera entity targeting `http://<ip>:8080/?action=stream`.
- **Architecture**
Expand All @@ -77,10 +77,10 @@ Treat this file as the living source of truth for workflows and expectations—u
- `__init__.py` – Config entry setup, HTTP client initialization, coordinator registration, teardown.
- `config_flow.py` – Discovery + manual onboarding, reauth + reconfigure flows, credential validation via HTTP, options flow (scan interval + LED-availability override). Enforces `SUPPORTED_PIDS` early via `_is_supported_detail()`.
- `coordinator.py` – `DataUpdateCoordinator` wrapping `FlashForgeClient.info.get()` with graceful error handling and cleanup.
- `sensor.py` – 28 sensor entities (operational + diagnostic). Modify the `SENSORS` tuple, translations, and docs together when changing sensors.
- `binary_sensor.py` – 4 machine-state binary sensors (printing, online, error, paused).
- `switch.py` – LED switch with client capability check (capability check can be overridden via options).
- `select.py` – Filtration mode select (Off / Internal / External, AD5X only).
- `sensor.py` – 38 sensor entities (operational + diagnostic). `SENSORS` is composed of `_BASE_SENSORS + TOOLHEAD_SENSORS + CHAMBER_SENSORS` (per-toolhead and heated-chamber sensors are gated on the Creator 5 series). Modify the tuples, translations, and docs together when changing sensors.
- `binary_sensor.py` – 5 machine-state binary sensors (printing, online, error, paused, door-open). `door_open` is availability-gated on `has_door_sensor` (Creator 5 Pro only).
- `switch.py` – LED switch with client capability check (capability check can be overridden via options) and the camera switch. Descriptions carry both an `availability_fn` (greys the entity out; use when the printer may report the feature later) and a `supported_fn` (skips creating it entirely; use when the model's API cannot perform the action at all — the Creator 5 camera switch is inert, so it is never created there).
- `select.py` – Filtration mode select (Off / Internal / External; availability gated on `is_pro OR is_creator5_pro`, i.e. Adventurer 5M Pro / Creator 5 Pro).
- `button.py` – Pause / resume / cancel / clear-status commands; request a refresh after each action.
- `camera.py` – MJPEG camera entity (`http://<ip>:8080/?action=stream` by default).
- `image.py` – Hosts the active-print g-code thumbnail entity AND the 4 Material Station slot swatch entities (AD5X / Creator 5 series). Swatches are PNG-encoded by `render_swatch_bytes()` (Pillow) inside an executor; both entity types cache rendered bytes and only invalidate on input change.
Expand Down Expand Up @@ -237,7 +237,7 @@ The local Home Assistant instance runs in **WSL2 only** with the following setup
1. **Implementation**
- Keep everything async; no blocking calls inside Home Assistant callbacks.
- Use HTTP-facing client methods (`client.info`, `client.control`, `client.job_control`, etc.).
- Respect capability flags (`client.led_control`, `client.filtration_control`) before exposing features.
- Respect capability flags (`client.led_control` for the LED switch) before exposing features. Do NOT trust the `/product`-derived `client.filtration_control` — gate filtration/TVOC/chamber-fan on model identity (`is_pro OR is_creator5_pro`) instead.
2. **Localization & Docs**
- Update `strings.json` and `translations/en.json` whenever UI text changes.
- Reflect behavior changes in `README.md`, `CHANGELOG.md`, `CLAUDE.md`, and `AGENTS.md` as appropriate.
Expand Down Expand Up @@ -282,7 +282,7 @@ pytest tests/unit/ --cov=custom_components.flashforge --cov-report=term-missing
pytest tests/unit/test_sensor_value_functions.py -v
```

**Current coverage (103 tests total):**
**Current coverage (125 tests total):**
- `tests/unit/test_discovery.py` – printer discovery protocol
- `tests/unit/test_sensor_value_functions.py` – sensor value extraction
- `tests/unit/test_binary_sensor_value_functions.py` – binary sensor logic
Expand Down Expand Up @@ -338,13 +338,13 @@ pytest tests/unit/test_sensor_value_functions.py -v
3. Add the integration via UI; test both discovery and manual paths.
4. Open the created device and verify entities:
- Sensors: machine status, nozzle temps/targets, bed temps/targets, progress, file, current/total layers, elapsed/remaining time, filament length/weight, print speed, z offset, nozzle size, filament type, lifetime stats, plus diagnostic sensors (firmware version, free disk space, error code).
- Binary sensors: printing, online, error, paused.
- Binary sensors: printing, online, error, paused, door-open (Creator 5 Pro only).
- Switch: LED (may show unavailable on unsupported models unless override is enabled).
- Select: filtration mode — Off / Internal / External (AD5X only).
- Select: filtration mode — Off / Internal / External (Adventurer 5M Pro / Creator 5 Pro only).
- Buttons: pause, resume, cancel, clear status.
- Camera: MJPEG feed reachable.
- Image: g-code thumbnail of the active print.
- Image (AD5X only): four IFS slot swatches (`image.*_ifs_slot_1..4`) showing material color + label, "EMPTY" tile for unloaded slots.
- Image (AD5X / Creator 5 series): four Material Station slot swatches (`image.*_ifs_slot_1..4`) showing material color + label, "EMPTY" tile for unloaded slots.
5. Trigger control actions (pause/resume/cancel, switches) and ensure states refresh.
6. Observe coordinator error handling by temporarily disconnecting the printer and confirming entities surface availability correctly.

Expand All @@ -356,16 +356,16 @@ pytest tests/unit/test_sensor_value_functions.py -v
## Implementation Guard Rails
- **HTTP-first policy** – Do not introduce direct TCP/G-code communication here. If unavoidable, extend the API library (`ff-5mp-api-py`) and consume it via HTTP-style helpers.
- **Coordinator as source of truth** – Entities derive state from the coordinator’s latest `FFMachineInfo`. Avoid storing custom copies of printer state in entities.
- **PID for model identity, never the printer name** – Modern HTTP printers report a stable firmware-set integer `pid` on `/detail` (35 = Adventurer 5M, 36 = 5M Pro, 38 = AD5X). The integration enforces this in TWO places that should both stay in sync:
- `config_flow.py` `_is_supported_detail()` reads the raw `/detail` payload during pairing and rejects PIDs not in `SUPPORTED_PIDS = {35, 36, 38}`. This is the early gate — runs before any `FFMachineInfo` parsing happens.
- The library (`flashforge-python-api>=1.2.3`) populates `FFMachineInfo.is_pro` / `is_ad5x` / `pid` from the same value. This is the runtime gate — used by `switch.py` for LED / filtration availability.
- **PID for model identity, never the printer name** – Modern HTTP printers report a stable firmware-set integer `pid` on `/detail` (35 = Adventurer 5M, 36 = 5M Pro, 38 = AD5X, 40 = Creator 5, 41 = Creator 5 Pro). The integration enforces this in TWO places that should both stay in sync:
- `config_flow.py` `_is_supported_detail()` reads the raw `/detail` payload during pairing and rejects PIDs not in `SUPPORTED_PIDS = {35, 36, 38, 40, 41}`. This is the early gate — runs before any `FFMachineInfo` parsing happens.
- The library (`flashforge-python-api>=1.3.0`) populates `FFMachineInfo.is_pro` / `is_ad5x` / `is_creator5` / `is_creator5_pro` / `pid` from the same value. This is the runtime gate — used by `switch.py` for LED availability and by `sensor.py` / `select.py` for model-identity capability gating.
- Both gates are needed: the config-flow gate stops unsupported hardware from being added at all; the runtime gate keeps capability flags accurate after pairing. Do NOT substring-match `info.name` — it's user-mutable and broke detection in v1.1.8 (see issue #13 / v1.1.9 fix). When new modern PIDs ship, update `SUPPORTED_PIDS` here AND coordinate a library bump.
- **Error handling** – Wrap connection issues in `ConfigEntryNotReady`, `ConnectionError`, or `UpdateFailed` so Home Assistant retries gracefully.
- **Entity additions**
- Add to the appropriate entity tuple.
- Provide unique `key`, icon, units, and defensive `value_fn`.
- Update documentation (README, CHANGELOG, CLAUDE/AGENTS) and translations.
- **Options flow** – Currently only the scan interval. Extend cautiously to avoid breaking existing entries.
- **Options flow** – Currently exposes the scan interval and the LED-availability override. Extend cautiously to avoid breaking existing entries.

## Release & Publishing Checklist
1. Implement and document changes.
Expand Down
11 changes: 9 additions & 2 deletions custom_components/flashforge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
override_led_availability = entry.options.get(CONF_OVERRIDE_LED_AVAILABILITY, False)

# Create FlashForge client
# Create FlashForge client.
#
# `led_control_override` is tri-state, not a boolean: None means "no
# override, trust /product", True forces the capability on, and False forces
# it OFF. Passing the option straight through sent False whenever the user
# had not enabled it - the default - which vetoed the printer's own correct
# capability report and greyed out the LED switch on every model. Only ever
# force the capability when the user actually asked for it.
client = FlashForgeClient(
ip_address=ip_address,
serial_number=serial_number,
check_code=check_code,
options=FiveMClientConnectionOptions(
led_control_override=override_led_availability,
led_control_override=True if override_led_availability else None,
),
)

Expand Down
Loading
Loading