Add toggle to disable printer firmware update checks

Add new setting "Check printer firmware" in Settings → General → Updates
that allows users to disable automatic firmware update checks from
Bambu Lab servers.

Closes #169
This commit is contained in:
maziggy
2026-01-29 07:50:32 +01:00
parent c123fa08c0
commit a367ddc66e
13 changed files with 178 additions and 111 deletions
+3
View File
@@ -5,6 +5,9 @@ All notable changes to Bambuddy will be documented in this file.
## [0.1.6-final] - Not released
### New Features
- **Disable Printer Firmware Checks** - New toggle in Settings → General → Updates to disable printer firmware update checks:
- Prevents Bambuddy from checking Bambu Lab servers for firmware updates
- Useful for users who prefer to manage firmware manually or have network restrictions
- **Archive Plate Browsing** - Browse plate thumbnails directly in archive cards (Issue #166):
- Hover over archive card to reveal plate navigation for multi-plate files
- Left/right arrows to cycle through plate thumbnails
+1
View File
@@ -74,6 +74,7 @@ async def get_settings(db: AsyncSession = Depends(get_db)):
"capture_finish_photo",
"spoolman_enabled",
"check_updates",
"check_printer_firmware",
"virtual_printer_enabled",
"ftp_retry_enabled",
"mqtt_enabled",
+2
View File
@@ -26,6 +26,7 @@ class AppSettings(BaseModel):
# Updates
check_updates: bool = Field(default=True, description="Automatically check for updates on startup")
check_printer_firmware: bool = Field(default=True, description="Check for printer firmware updates from Bambu Lab")
# Language
notification_language: str = Field(default="en", description="Language for push notifications (en, de)")
@@ -134,6 +135,7 @@ class AppSettingsUpdate(BaseModel):
spoolman_url: str | None = None
spoolman_sync_mode: str | None = None
check_updates: bool | None = None
check_printer_firmware: bool | None = None
notification_language: str | None = None
ams_humidity_good: int | None = None
ams_humidity_fair: int | None = None
@@ -215,6 +215,28 @@ class TestSettingsAPI:
assert result["currency"] == "JPY"
assert result["check_updates"] is False
@pytest.mark.asyncio
@pytest.mark.integration
async def test_update_check_printer_firmware(self, async_client: AsyncClient):
"""Verify check_printer_firmware can be updated."""
# Default should be True
response = await async_client.get("/api/v1/settings/")
assert response.json()["check_printer_firmware"] is True
# Update to False
response = await async_client.put("/api/v1/settings/", json={"check_printer_firmware": False})
assert response.status_code == 200
assert response.json()["check_printer_firmware"] is False
# Verify persistence
response = await async_client.get("/api/v1/settings/")
assert response.json()["check_printer_firmware"] is False
# Update back to True
response = await async_client.put("/api/v1/settings/", json={"check_printer_firmware": True})
assert response.status_code == 200
assert response.json()["check_printer_firmware"] is True
# ========================================================================
# MQTT settings tests
# ========================================================================
@@ -29,6 +29,7 @@ describe('Layout', () => {
http.get('/api/v1/settings/', () => {
return HttpResponse.json({
check_updates: false,
check_printer_firmware: false,
auto_archive: true,
});
}),
@@ -105,6 +105,7 @@ describe('FileManagerPage', () => {
http.get('/api/v1/settings/', () => {
return HttpResponse.json({
check_updates: false,
check_printer_firmware: false,
library_disk_warning_gb: 5,
});
}),
@@ -31,6 +31,7 @@ const mockSettings = {
ha_url: '',
ha_token: '',
check_updates: false,
check_printer_firmware: false,
};
describe('SettingsPage', () => {
@@ -124,6 +125,16 @@ describe('SettingsPage', () => {
expect(screen.getByText('Appearance')).toBeInTheDocument();
});
});
it('shows updates section with firmware toggle', async () => {
render(<SettingsPage />);
await waitFor(() => {
expect(screen.getByText('Updates')).toBeInTheDocument();
expect(screen.getByText('Check for updates')).toBeInTheDocument();
expect(screen.getByText('Check printer firmware')).toBeInTheDocument();
});
});
});
describe('tabs navigation', () => {
@@ -48,6 +48,7 @@ const mockArchives = [
const mockSettings = {
currency: '$',
check_updates: false,
check_printer_firmware: false,
};
const mockFailureAnalysis = {
+1
View File
@@ -681,6 +681,7 @@ export interface AppSettings {
energy_cost_per_kwh: number;
energy_tracking_mode: 'print' | 'total';
check_updates: boolean;
check_printer_firmware: boolean;
notification_language: string;
// AMS threshold settings
ams_humidity_good: number; // <= this is green
+6 -1
View File
@@ -915,6 +915,7 @@ function PrinterCard({
timeFormat = 'system',
cameraViewMode = 'window',
onOpenEmbeddedCamera,
checkPrinterFirmware = true,
}: {
printer: Printer;
hideIfDisconnected?: boolean;
@@ -932,6 +933,7 @@ function PrinterCard({
timeFormat?: 'system' | '12h' | '24h';
cameraViewMode?: 'window' | 'embedded';
onOpenEmbeddedCamera?: (printerId: number, printerName: string) => void;
checkPrinterFirmware?: boolean;
}) {
const queryClient = useQueryClient();
const navigate = useNavigate();
@@ -992,12 +994,13 @@ function PrinterCard({
refetchInterval: 30000, // Fallback polling, WebSocket handles real-time
});
// Check for firmware updates (cached for 5 minutes)
// Check for firmware updates (cached for 5 minutes, can be disabled in settings)
const { data: firmwareInfo } = useQuery({
queryKey: ['firmwareUpdate', printer.id],
queryFn: () => firmwareApi.checkPrinterUpdate(printer.id),
staleTime: 5 * 60 * 1000,
refetchInterval: 5 * 60 * 1000,
enabled: checkPrinterFirmware,
});
// Collect unique tray_info_idx values for cloud filament info lookup
@@ -4648,6 +4651,7 @@ export function PrintersPage() {
timeFormat={settings?.time_format || 'system'}
cameraViewMode={settings?.camera_view_mode || 'window'}
onOpenEmbeddedCamera={(id, name) => setEmbeddedCameraPrinters(prev => new Map(prev).set(id, { id, name }))}
checkPrinterFirmware={settings?.check_printer_firmware !== false}
/>
))}
</div>
@@ -4676,6 +4680,7 @@ export function PrintersPage() {
timeFormat={settings?.time_format || 'system'}
cameraViewMode={settings?.camera_view_mode || 'window'}
onOpenEmbeddedCamera={(id, name) => setEmbeddedCameraPrinters(prev => new Map(prev).set(id, { id, name }))}
checkPrinterFirmware={settings?.check_printer_firmware !== false}
/>
))}
</div>
+19
View File
@@ -407,6 +407,7 @@ export function SettingsPage() {
settings.energy_cost_per_kwh !== localSettings.energy_cost_per_kwh ||
settings.energy_tracking_mode !== localSettings.energy_tracking_mode ||
settings.check_updates !== localSettings.check_updates ||
(settings.check_printer_firmware ?? true) !== (localSettings.check_printer_firmware ?? true) ||
settings.notification_language !== localSettings.notification_language ||
settings.ams_humidity_good !== localSettings.ams_humidity_good ||
settings.ams_humidity_fair !== localSettings.ams_humidity_fair ||
@@ -469,6 +470,7 @@ export function SettingsPage() {
energy_cost_per_kwh: localSettings.energy_cost_per_kwh,
energy_tracking_mode: localSettings.energy_tracking_mode,
check_updates: localSettings.check_updates,
check_printer_firmware: localSettings.check_printer_firmware,
notification_language: localSettings.notification_language,
ams_humidity_good: localSettings.ams_humidity_good,
ams_humidity_fair: localSettings.ams_humidity_fair,
@@ -1296,6 +1298,23 @@ export function SettingsPage() {
<div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
</label>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-white">Check printer firmware</p>
<p className="text-sm text-bambu-gray">
Check for printer firmware updates from Bambu Lab
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={localSettings.check_printer_firmware ?? true}
onChange={(e) => updateSetting('check_printer_firmware', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
</label>
</div>
<div className="border-t border-bambu-dark-tertiary pt-4">
<div className="flex items-center justify-between mb-2">
<div>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -23,7 +23,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-abyjQYWw.js"></script>
<script type="module" crossorigin src="/assets/index-CKmo1TxA.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DFuUL8IF.css">
</head>
<body>