feat(printers): airduct mode + status badges + force refresh on printer card

Surface four Home Assistant-style controls on the Printers page card:

  - SD Card badge in the top status row (green / red, icon-only).
  - Enclosure Door badge in the top status row (green / yellow, icon-only).
    Detection per printer family — X1/X1C/X1E read home_flag bit 23, all
    others read top-level `stat` (hex string) bit 23 — so X1 firmware that
    does not flip stat bit 23 stops false-triggering "open". WebSocket
    status-change dedup key now includes door_open so toggling the door
    alone publishes a push, no 30s REST-poll wait.
  - Airduct Mode badge beside the speed control (cooling / heating)
    for P2S/H2D/H2C/H2S; one-click dropdown calls the existing
    set_airduct MQTT command via a new POST /printers/{id}/airduct-mode
    route.
  - Force Refresh entry in the kebab menu — calls the existing
    /printers/{id}/refresh-status endpoint to request a pushall snapshot
    without forcing a reconnect.

  Tests: door-open parsing (X1 home_flag, non-X1 stat, ignore mismatched
  source, invalid hex) and airduct route (validation, not-connected,
  success, failure).
This commit is contained in:
maziggy
2026-04-13 12:50:17 +02:00
parent eec7793955
commit 8af0966e68
23 changed files with 862 additions and 446 deletions
+35
View File
@@ -206,3 +206,38 @@ describe('FormData requests include auth header', () => {
expect(capturedHeaders!.get('Authorization')).toBe('Bearer test-token');
});
});
describe('Printer control endpoints', () => {
it('refreshPrinterStatus POSTs to /printers/:id/refresh-status', async () => {
let calledUrl: string | null = null;
let calledMethod: string | null = null;
server.use(
http.post('/api/v1/printers/:id/refresh-status', ({ request, params }) => {
calledUrl = `/printers/${params.id}/refresh-status`;
calledMethod = request.method;
return HttpResponse.json({ status: 'ok' });
}),
);
const result = await api.refreshPrinterStatus(7);
expect(calledMethod).toBe('POST');
expect(calledUrl).toBe('/printers/7/refresh-status');
expect(result).toEqual({ status: 'ok' });
});
it('setAirductMode passes mode in query string', async () => {
let capturedUrl = '';
server.use(
http.post('/api/v1/printers/:id/airduct-mode', ({ request }) => {
capturedUrl = request.url;
return HttpResponse.json({ success: true, message: 'ok' });
}),
);
await api.setAirductMode(3, 'cooling');
expect(capturedUrl).toContain('mode=cooling');
await api.setAirductMode(3, 'heating');
expect(capturedUrl).toContain('mode=heating');
});
});