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-21 10:35:53 +02:00
parent 25a23eadd7
commit 6b517ddc63
5 changed files with 150 additions and 54 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 (
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -26,7 +26,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-Ckvy6dRm.js"></script>
<script type="module" crossorigin src="/assets/index-BsijWO1C.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-7mua31Zk.css">
</head>
<body>