Commit Graph
1016 Commits
Author SHA1 Message Date
maziggy 6954cff3ee Turn on the chamber light while the camera is in use
A printer kept dark between uses showed a black live view and sent black
notification photos (#1655). Settings → Camera can turn the light on for
all printers or for selected ones, for the live view, notification
photos, the finish photo and the snapshot API that Home Assistant uses,
and turns it off 15 seconds after the last use. It only turns off a
light it turned on, and switching the light by hand hands it back.
Snapshots wait an adjustable delay after the light comes on. Timelapse,
the Obico check and the in-print frame bank don't switch it, since they
would make it flash.

The camera card no longer lists every printer: external cameras are
added from a searchable printer picker and only those printers are
listed, so large farms get a short page.
2026-10-04 11:26:07 +02:00
Adam Spice 3596eb0c31 Create streaming overlay tokens and import existing URLs (#3207) 2026-10-04 09:37:14 +02:00
WATARIE fa25956738 Easy management of printer locations (groups) (issue #2962)- #2970 (#2995) 2026-10-03 14:28:35 +02:00
M2ABRAMSTANK 3ce9b917cb fix(db): cap SQLite pool fd bill + raise container nofile — prevents fd-exhaustion DB corruption (#2884) 2026-10-03 13:44:50 +02:00
Martin Grolmus 0a3e341584 Map each printer against its own AMS, and stop dispatching a job whose filament is not loaded (#2799) (#2803) 2026-10-03 13:28:26 +02:00
maziggyandKeybored02 12e543b1b4 Fix SpoolBuddy reading the filament type as the Bambu spool ID (#984)
The daemon took tray_uuid from tag blocks 4-5, which hold the filament
type, so every spool of one type sent the same value and the kiosk never
saved it. Read block 9, which both tags of a spool share and the AMS
reports as tray_uuid. Save it on kiosk quick-add and link in local and
Spoolman mode, and give spools added before the fix their tray_uuid when
their exact tag UID is scanned again. Drop any tray_uuid that is not a v4
UUID, so daemons that are not updated yet keep matching by tag UID.

Block 9 read based on #1200 by @Keybored02.

Co-Authored-By: Keybored02 <Keybored02@users.noreply.github.com>
2026-10-03 12:19:04 +02:00
Adam Spice 67d3e7e869 Add stream overlay logos and progress colours (#3208) 2026-10-03 09:33:13 +02:00
maziggy 098fa5273f Security hardening 2026-10-02 17:29:19 +02:00
maziggy bff7f68106 Put back a slot's K-profile when the printer loses it (#3219) 2026-10-02 10:04:48 +02:00
maziggy e0149ae4ac Restart a camera stream that keeps repeating one frame (#3218, #3189)
After an RTSP session dropped, ffmpeg could keep writing the same JPEG
indefinitely with no connection to the printer left (two hours at ~29 fps
on a P2S), and every check that counts frames saw a healthy stream. The
stream now restarts ffmpeg after 20 s without a changed frame, on the
existing reconnect path, while viewers stay attached. If the first frame
after the restart is the same picture again, the camera really shows a
still scene (a dark, idle chamber): it is then only re-checked every
5 minutes until the picture changes.
2026-10-02 08:50:19 +02:00
maziggy 02e39b18b9 Configure Orca Cloud filaments with their own filament ID (#3216)
OrcaSlicer's Sync filaments finds a slot's preset by the slot's filament
ID alone. The Configure dialog looked up an Orca profile's ID in the
browser and quietly sent the generic for the material when that came back
empty, so Orca custom filaments reached the slicer as Generic and the log
showed nothing. configure now resolves the ID on the server from
orca_profile_id, follows inherits to the parent profile or the Bambu
filament it was copied from, logs the outcome and reports a fallback,
which the dialog shows as a warning.

Slot presets also store the filament ID they were written with, so the
slot card and the Configure dialog stop showing a preset once the slot is
changed from OrcaSlicer's Device tab or the printer.

Re-configuring a slot for another filament no longer carries over the old
filament's active K-profile, which switched the slot back to the old
filament.
2026-10-02 08:32:57 +02:00
maziggy 0165dafd29 Stop firmware lookups when "Check printer firmware" is off 2026-10-01 16:27:07 +02:00
maziggy a81ac2cfb5 Keep the Announcements entry for whoever may see announcements 2026-10-01 15:59:54 +02:00
maziggy 6968433d70 Show the job the scheduler starts next on the printer card (#3200) 2026-10-01 12:58:13 +02:00
maziggy 5eb37cd41c Show announcements from the Bambuddy maintainers
Fetch a signed feed.json from the public bambuddy-notifications repo on
GitHub at startup and every 6 hours. Nothing about the install is sent;
targeting (version, beta channel, install type) is decided locally.

- Ed25519 against a key built into the app; an older serial is refused so a
  withdrawn message can't come back. The feed replaces the stored list, and
  a failed or rejected fetch keeps the last good one.
- Sidebar entry above System with an unread count, a slide-over list, and a
  banner for unread important/critical messages. Read state per user.
- Admins by default; Settings > General > Updates can show them to all
  users or switch them off, which also stops the fetch.
- Plain text only; links to github.com and bambuddy.cool only.
2026-10-01 10:45:06 +02:00
maziggy 1cc4bae5d2 Report a refused RFID refresh, and fall back to M620 R (#3206)
ams_get_rfid was published and reported as success without reading the
printer's answer. X1Plus on base 01.08.02.00 answers FAIL / ERROR STATE,
so the user saw "Refreshing" and the K profile was re-applied to a slot
that was never read.

- Wait for the ams_get_rfid answer (matched by sequence_id).
- On a refusal from an idle X1/P1/A1, send the legacy M620 R<ams*4+slot>
  gcode Bambu Studio uses for those models, AMS units 0-3 only. Never
  during a job, never on newer-protocol models; printers that accept
  ams_get_rfid never see it.
- Refused: the route returns 400 with the printer's reason and no PA
  re-apply is scheduled. No answer still counts as accepted.
- Log the answers at INFO so refusals reach support bundles.
2026-10-01 09:40:00 +02:00
Benji 7f64e2ba8b Add feature: Attach camera snapshots to notifications (issue #3089) (#3199) 2026-10-01 08:43:44 +02:00
adman234 b67c9b572c Add "Combine to 3MF" for slicing several STLs on one plate (#3162) 2026-10-01 08:11:19 +02:00
maziggy e9627e44a2 Start queued jobs in list order, whether pinned or "Any <model>" (#3200) 2026-09-30 15:55:05 +02:00
maziggy 8b5d8e3170 Add layers, job id, HMS faults and serial to the API-key printer status (#2919) 2026-09-30 11:45:31 +02:00
maziggy bf7fc67dc0 Choose what a spool label shows, preview it, and save it as PNG (#2981, #2980) 2026-09-30 11:08:05 +02:00
Kouki Ojima cb924e5c4a Match a Bambu roll to its own product line, not just its colour (issue #2907) (#2944) 2026-09-30 10:15:42 +02:00
maziggy 29e6d28205 Resolve LDAP groups on lldap and OpenLDAP (#3197)
lldap and OpenLDAP's memberof overlay omit memberOf from "*", so every
lldap login fell through to the default group. Request memberOf by name
when the schema defines it, and on non-AD directories also search the
directory root for groupOfNames/groupOfUniqueNames entries listing the
user, since groups often sit outside the user search base and the
overlay tracks only one group class.

Also: skip ldap3's anonymous schema read after StartTLS, which AD and
Samba AD reject, so StartTLS works there; reword a server's StartTLS
refusal with an LDAPS hint; stop the bundle sanitizer masking part of
an OID as an IP; skip the sync right after auto-provisioning so the
default-group warning logs once.
2026-09-30 09:46:54 +02:00
maziggy 2f7f17a891 Use the Spoolman spool's own initial_weight as its label weight (#3194)
Spoolman keeps a full spool's net weight on the spool (initial_weight)
and falls back to the filament's weight, but Bambuddy read only the
filament, so a 250 g spool of a 1000 g filament showed and synced as
1000 g. The list, weigh, AMS sync, SpoolBuddy scale, remain-% tracking,
fill bar and cost now share one lookup. Create writes initial_weight,
and a label-weight edit writes it instead of patching or duplicating the
filament. The spool form's cost per kg is converted at the spool's size
to and from Spoolman's per-spool price.
2026-09-30 09:12:45 +02:00
maziggy c145b3ebc1 Use the vendor's empty spool weight as tare in Spoolman mode (#3195)
Spoolman resolves a spool's tare from the spool, then the filament, then
the vendor's empty_spool_weight. Bambuddy skipped the vendor and fell
back to 250 g, so weighing a spool whose tare was set only on its vendor
gave the wrong remaining weight. The inventory weigh action, the
SpoolBuddy scale and the displayed core weight now share one lookup, and
"keep old weight" on a filament change stamps a vendor-inherited tare.
2026-09-30 08:43:21 +02:00
Thomansky 1c316e2ad6 Answer the outcome prompt by reacting to the Telegram message (#3129) 2026-09-29 16:06:51 +02:00
William Faircloth d04514c842 Sync OIDC provider groups to BamBuddy groups on every login (issue #3107) (#3122) 2026-09-29 15:49:44 +02:00
Thomansky 71d4b2f70d Material number as a first-class spool field (#2994) 2026-09-29 15:11:50 +02:00
maziggy 926957f648 Post work-2 PR #2990 2026-09-29 14:54:36 +02:00
Thomansky 226826f3bd File Manager previews: fullscreen and zoom, image previews, double-click to open (#2990) 2026-09-29 14:27:19 +02:00
maziggy 2315b8fb5d Count camera reconnects in a row, not for the life of the stream 2026-09-29 10:28:41 +02:00
maziggy 3497cf0d46 Hold an automatic slot unlink until the slot stays empty (issue #3186) 2026-09-29 09:37:04 +02:00
Thomansky 053cfa73ad Suppliers as a managed list with per-spool assignments (#2996) 2026-09-28 15:14:07 +02:00
Kouki Ojima b6521e93f8 Let a spool keep its own empty weight (issue #2908) (#3011) 2026-09-28 14:48:04 +02:00
maziggy 63e987e591 Paint SpoolBuddy spools with their extra colours and effect (issue #3033) 2026-09-28 14:27:21 +02:00
maziggy 033eff1254 Show Bambu's own HMS descriptions and the real alert level (issue #2728)
Document HMS levels, Bambu's descriptions and uncounted faults (issue #2728)
2026-09-28 13:57:02 +02:00
M2ABRAMSTANK 6c0292b09a fix(ams): show a parked drying command as not started instead of an active cycle (#3096) 2026-09-28 12:09:59 +02:00
maziggy 0cc7cf0f36 Refuse null for number and on/off settings, and read bad rows as the default (issue #2518)
A PUT /settings with null for a number setting stored the literal
"None". From then on int()/float() raised inside the response builder,
and every settings read returned 503 until the row was fixed by hand.

Explicit null for any boolean or numeric setting is now refused with a
422 that names the keys, and nothing in that request is saved. A numeric
row that does not parse reads back as its default with a warning, so an
install that already stored one recovers. The typed-key lists moved to
module constants so the save check and the read path share them.
2026-09-28 11:32:02 +02:00
M2ABRAMSTANK 568702486b feat: Wait for sustained AMS humidity before an ambient auto-dry starts (issue #2518) 2026-09-28 11:10:23 +02:00
Kouki Ojima ff07a82358 Keep the consumed-counter reset out of Spoolman's own numbers (issue #2906) (#2939) 2026-09-28 09:58:16 +02:00
maziggy 6855d65d12 Let other applications send messages through the notification channels
POST /notifications/app-message delivers an app's message to every channel
with the new "Messages from connected apps" switch on (off by default),
through quiet hours, the digest and the log. API keys need the new "Send
notifications" permission, and their owner notifications:update; plain text,
http(s) links, 20 messages a minute per key. The electricity-price door and
this one now share one scoped-key check. /queue?batch=<id> opens and
highlights one batch order.
2026-09-27 12:45:50 +02:00
Adam Spice 858f8c772f feat: add optional printer model to stream overlay (#3099) 2026-09-27 12:04:05 +02:00
Thomansky 44c7e6fb39 Post-print outcome confirmation: good/reject verdicts, one-tap links, yield stats (#3047) 2026-09-26 15:37:19 +02:00
maziggy d56b48c499 feat(auth): connected apps - sign in to external applications with Bambuddy
Minimal OAuth 2.0 authorization-code flow with PKCE (S256): admins register
an app with one exact callback URL (Settings > API Keys > Connected Apps);
/connect/authorize asks for consent once and returns a single-use, 60 s code
bound to app, callback and challenge; POST /api/v1/connect/token swaps it,
with the client secret, for the user's identity and permissions. Codes and
secrets stored hashed, exchanges rate-limited per client and IP, no redirect
before the callback is validated, API keys cannot authorize, refused while
auth is disabled. i18n for all 15 locales.

-----

fix(db): upgrading from 0.2.4.0 or older no longer crashes at startup

The #2974 failure-reason conversion ran before the #1378 migration that adds
print_log_entries.failure_reason, so older databases stopped with "no such
column: failure_reason". It now skips a table without the column, only runs
where a legacy label exists, and on SQLite rebuilds archive_fts first, since
archives created before that index existed trip "database disk image is
malformed" when updated.
2026-09-26 12:51:53 +02:00
maziggy 7c16079ffa feat(queue): let a batch record the external order it fulfils
POST /queue/batches accepts external_source + external_ref; both are
returned on every batch and filterable on GET /queue/batches. The pair is
unique (index uq_print_batches_external), so a retried create answers 409
instead of queueing the same order twice. Migration covers SQLite and
PostgreSQL.
2026-09-26 12:18:40 +02:00
maziggy 86bd9e1bd4 fix(file-manager): harden the merged previews - PDF.js legacy build, server PDF thumbnails, STEP progress (issue #2976)
Follow-ups after merging PR #3128 (with the #2990 preview work):

- PDF preview failed on every browser without
  Map.prototype.getOrInsertComputed (Chrome < 145, Firefox < 144,
  older Safari): "This file cannot be previewed" for any PDF. Load
  pdf.js's legacy build, which bundles the polyfills for page and
  worker, and bundle the worker through Vite (?worker&url) so
  build.target lowers its class static block for Safari 16.0-16.3.
  The browser-baseline check only scanned .js output and never saw
  the copied .mjs worker; it scans .mjs too.
- PDF grid thumbnails only existed after someone opened the preview.
  Page one is now rendered server-side with PDFium (pypdfium2, new
  dependency, prebuilt wheels for every shipped platform) on upload,
  ZIP extraction and external-folder scans; Generate Thumbnails
  backfills PDFs as well. Renders are serialised behind a lock
  (PDFium is not thread-safe), run off the event loop, and scale
  from the page size so a huge MediaBox cannot allocate a huge
  bitmap. Unreadable PDFs land without a thumbnail and keep the
  browser fallback.
- A large STEP file takes over a minute to mesh in the browser and
  showed only a spinner. STEP loads now show "Converting STEP
  model... N s" and a note that large files can take a minute or
  more, in all 15 locales.
- Drop the two occt-import-js "externalized for browser
  compatibility" build warnings (path/crypto are only required in
  its Node branch); any other externalization still shows.
2026-09-26 09:51:52 +02:00
Thomansky 12dddada0a File Manager: external link, notes and photos on library files (#3128) 2026-09-26 08:56:48 +02:00
maziggy 6e8d543d2a fix(ams): stop showing the humidity drop index as a percentage (issue #3140)
Bambu sends two humidity fields that are not the same quantity.
humidity_raw is relative humidity in percent; humidity is a 1-5 drop
index, and it runs the other way -- OpenBambuAPI's push_info sample
pairs "humidity:30%" with "humidity_idx:4", so a high index means dry
where a high percentage means wet.

Four call sites used the index whenever no percentage arrived. A unit
sending only the index therefore rendered as "2%" in the green band
while being the second-wettest of the five steps, charted an average of
index values as a percentage, and sat under every humidity threshold
forever, since no index can reach one -- the alarm and auto-drying could
not fire for such a unit at all.

- utils/ams_humidity: one leaf helper, a percentage or None. The index
  is never converted; None is what every caller already handles.
- routes/printers, printer_manager, print_scheduler, main, bambu_mqtt:
  all five readings go through it, so the card, the websocket, the
  chart, the alarm and auto-drying cannot answer differently.
- main: a unit that reports the index and no usable percentage says so
  once per unit in the log, with its firmware versions requested. No
  supported printer is known to do this, and "known" is doing work
  there -- the alternative is a card that goes blank with no trace.

Three faults found while checking what else those paths touched:

- main: humidity_raw=float(x) if x else None stored NULL for a numeric
  0% while writing 0.0 to humidity on the same row.
- main: that same expression was unguarded, unlike the parse above it,
  so a non-numeric humidity_raw raised inside record_ams_history and
  aborted the pass for every printer, not just the one that sent it.
- routes/ams_history: the averages were tested for truthiness, so a
  window averaging exactly 0 reported no average while the min and max
  beside it reported 0.0.

An affected unit now reports no humidity rather than a number that means
the opposite: the indicator is hidden, the chart leaves a gap, the alarm
and auto-drying skip the unit. Temperature is untouched. Auto-drying's
outcome is unchanged either way -- an index could never cross the
threshold -- so only the intent moves.

No supported printer is known to be affected; the report came from an
install running X1Plus, which Bambuddy does not support. Verified
against 7927 recorded samples from seven AMS units including an AMS-HT:
not one used the fallback. Two percentages that did fall through to the
index no longer do -- a reading with a decimal point, and "38.0", which
int() rejected.
2026-09-24 11:59:30 +02:00
maziggy 58ea7a360d refactor(models): break the schema cycle that backup and restore sort through
print_archives.library_file_id -> library_files.folder_id ->
library_folders.archive_id -> print_archives. Three nullable SET NULL
links, each reasonable alone, that together made a loop
metadata.sorted_tables could not sort: it dropped those edges, warned on
every backup and every restore, and could return an order placing a
child before its parent -- which once imported library_files ahead of
library_folders and killed a restore on a ForeignKeyViolation.

The restore no longer depends on that order (it strips every foreign key
before importing and adds them back after), but the backup export sorts
the same way, and the warning ends with "may raise an error in a future
release" -- which would break backup and restore on one upgrade.

Marking one edge use_alter removes it from the sort graph, not from the
database: PostgreSQL emits it as ALTER TABLE ADD CONSTRAINT, as it
already did for every constraint on these three tables, and SQLite
inlines it into CREATE TABLE, so ON DELETE SET NULL holds on both.
Verified against PostgreSQL 16 and SQLite.
2026-09-23 16:58:14 +02:00
maziggy 0eb083b32e fix(slicer): read the plate model once per part for the post-slice thumbnail (issue #3135)
The post-slice thumbnail loaded the sliced 3MF with trimesh, whose reader
mishandles the Bambu Studio / OrcaSlicer layout: for every component that
references a file in 3D/Objects/ it re-parses that file and appends all
of its meshes again. N copies of a part came back as N^2 copies of its
triangles, and each component carried every other part of the same file.
25 bins of 10k faces loaded as 6.4M faces; the render took 54 s and
8.4 GB on the event loop, and the server was OOM-killed.

Parse the 3MF directly (lxml, entities/DTD/network off, streamed and
freed element by element), walk objects, components and build items
(p:path on either) with their transforms, and keep each mesh once.
Decimate per unique mesh to its share of a face budget before placing
instances, flip winding on mirrored placements, and skip the thumbnail
above a face ceiling checked both on what decimation can reach and on
what it delivered.

Both slice routes run the render in a thread. The renderer uses
matplotlib's Figure/Agg API instead of pyplot, whose process-global
figure state let a threaded plate render and stl_thumbnail's
event-loop render lay out and close each other's figures.
2026-09-22 10:47:00 +02:00