fix(printers): hide chamber fan badge on open-frame Bambu models

The Printers page rendered three fan widgets (part / aux / chamber)
      for every printer unconditionally. Open-frame models (A1, A1 Mini,
      A2L, P1P) have no chamber fan — the firmware reports big_fan2_speed
      as 0, so the badge always rendered greyed-out and let users "set" a
      fan speed on hardware that doesn't exist.

      Adds MODELS_WITH_CHAMBER_FAN allowlist (X1C / X1 / X1E / X2D / P1S /
      P2S / H2D / H2D Pro / H2C / H2S) near mapModelCode, and the chamber
      entry is spread into fanItems only when the printer's model is in the
      set. Open-frame printers now show two badges (part + aux), which
      matches their actual hardware.

      Allowlist not denylist: mirrors the file's existing classification
      pattern (the enclosure-door badge gate uses the same shape), and the
      failure mode is preferable — a missing badge on a real chambered
      printer is obvious; a phantom badge on a future open-frame model
      would look correct and silently lie.
This commit is contained in:
maziggy
2026-06-28 12:39:32 +02:00
3 changed files with 103 additions and 7 deletions
+2
View File
File diff suppressed because one or more lines are too long
@@ -243,6 +243,73 @@ describe('PrintersPage', () => {
});
});
describe('fan badges', () => {
// Chamber fan only exists on enclosed Bambu models. Open-frame printers
// (A1, A1 Mini, A2L, P1P) have no chamber fan — the firmware reports
// big_fan2_speed as 0 there and the widget would be dead UI.
const statusWithFans = {
...mockPrinterStatus,
cooling_fan_speed: 53,
big_fan1_speed: 53,
big_fan2_speed: 53,
};
const renderWithPrinter = (printer: typeof mockPrinters[number]) => {
server.use(
http.get('/api/v1/printers/', () => HttpResponse.json([printer])),
http.get('/api/v1/printers/:id/status', () => HttpResponse.json(statusWithFans)),
);
render(<PrintersPage />);
};
it('hides chamber fan badge on A1 Mini (open-frame, no chamber fan)', async () => {
renderWithPrinter({ ...mockPrinters[0], model: 'A1 Mini' });
await waitFor(() => {
// Part-cooling badge confirms the fan row rendered.
expect(screen.getByTitle('Part Cooling Fan')).toBeInTheDocument();
});
expect(screen.getByTitle('Auxiliary Fan')).toBeInTheDocument();
expect(screen.queryByTitle('Chamber Fan')).not.toBeInTheDocument();
});
it('hides chamber fan badge on A1 (open-frame)', async () => {
renderWithPrinter({ ...mockPrinters[0], model: 'A1' });
await waitFor(() => {
expect(screen.getByTitle('Part Cooling Fan')).toBeInTheDocument();
});
expect(screen.queryByTitle('Chamber Fan')).not.toBeInTheDocument();
});
it('hides chamber fan badge on P1P (open-frame)', async () => {
renderWithPrinter({ ...mockPrinters[0], model: 'P1P' });
await waitFor(() => {
expect(screen.getByTitle('Part Cooling Fan')).toBeInTheDocument();
});
expect(screen.queryByTitle('Chamber Fan')).not.toBeInTheDocument();
});
it('shows chamber fan badge on X1C (enclosed)', async () => {
renderWithPrinter({ ...mockPrinters[0], model: 'X1C' });
await waitFor(() => {
expect(screen.getByTitle('Chamber Fan')).toBeInTheDocument();
});
expect(screen.getByTitle('Part Cooling Fan')).toBeInTheDocument();
expect(screen.getByTitle('Auxiliary Fan')).toBeInTheDocument();
});
it('shows chamber fan badge on P1S (enclosed)', async () => {
renderWithPrinter({ ...mockPrinters[0], model: 'P1S' });
await waitFor(() => {
expect(screen.getByTitle('Chamber Fan')).toBeInTheDocument();
});
});
});
describe('empty state', () => {
it('shows empty state when no printers', async () => {
server.use(
+34 -7
View File
@@ -1465,6 +1465,23 @@ function getStatusDisplay(state: string | null | undefined, stg_cur_name: string
}
}
// Bambu models that ship with an enclosure chamber fan (firmware field
// `big_fan2_speed`). Open-frame models (A1 / A1 Mini / A2L / P1P) have no
// chamber fan — `big_fan2_speed` is meaningless / always 0 there, so the
// widget is hidden in fanItems instead of rendered greyed-out.
const MODELS_WITH_CHAMBER_FAN: ReadonlySet<string> = new Set([
'X1C',
'X1',
'X1E',
'X2D',
'P1S',
'P2S',
'H2D',
'H2D Pro',
'H2C',
'H2S',
]);
// Map SSDP model codes to display names
function mapModelCode(ssdpModel: string | null): string {
if (!ssdpModel) return '';
@@ -3576,6 +3593,12 @@ function PrinterCard({
const statusControlClass = `relative text-center px-2 py-1.5 bg-bambu-dark rounded-lg flex-1 flex flex-col justify-center items-center transition-colors ${
canUseStatusControls ? 'cursor-pointer hover:bg-bambu-dark-tertiary' : 'cursor-default opacity-80'
}`;
// Chamber fan only exists on enclosed Bambu models. Open-frame
// printers (A1, A1 Mini, A2L, P1P) have no chamber fan — showing
// the widget there is at best dead UI and at worst suggests a
// control that does nothing. Mirrors the enclosure-door badge
// gate above.
const hasChamberFan = MODELS_WITH_CHAMBER_FAN.has(printer.model ?? '');
const fanItems = [
{
key: 'part',
@@ -3591,13 +3614,17 @@ function PrinterCard({
Icon: Wind,
activeClass: 'text-blue-400',
},
{
key: 'chamber',
label: t('printers.fans.chamber'),
value: status.big_fan2_speed ?? 0,
Icon: AirVent,
activeClass: 'text-green-400',
},
...(hasChamberFan
? [
{
key: 'chamber',
label: t('printers.fans.chamber'),
value: status.big_fan2_speed ?? 0,
Icon: AirVent,
activeClass: 'text-green-400',
},
]
: []),
];
return (