Assign Spoolman (Updater) Location/AMS Tray via NFC/QR

This solution connects Spoolman (filament management) with Home Assistant and the Spoolman Updater (e.g., tray/AMS mapping).

Spoolman Updater tracks filament usage automatically, and my automation streamlines the workflow for assigning filament spools to their locations and the AMS tray using just two scans, without needing to set the locations manually in Spoolman.

Using a QR code (just scan it with your camera app) or NFC tag, the spool and the storage location/tray are scanned.
It doesn’t matter whether you scan the location/tray tag or the spool tag first.

Home Assistant then sets the location in Spoolman (and optionally the active tray in Spoolman Updater) and sends a push notification to the Companion App via notify.

This is a short, informal video demonstrating my proof of concept (PoC):

1) Components Used

Spoolman

Spoolman is a web-based filament and spool management system. In this automation, the spool is updated via the REST API (PATCH) and the location field is set.

Spoolman Updater

Spoolman Updater can maintain additional datapoints related to spools/trays (e.g., active tray ID) and provides its own endpoints for that purpose.

Home Assistant Integrations (HACS)

  • Spoolman (HACS) - provides entities such as select.spoolman_spool_1_location.
  • Bambu Lab (HACS) - for printer/AMS/tray information.

2) Core Principle of the Automation

The automation reacts to the tag_scanned event. This event is generated when a QR code or NFC tag containing a link to URL-Based Tag - Home Assistant is scanned on the smartphone. Depending on the tag, the last scanned value is stored in helper entities. As soon as both a spool and a location or a tray are available, the following happens:

  • the spool ID is extracted from the spool tag,
  • the location name is taken from the location tag (or derived from the tray tag),
  • the location is set in Spoolman via REST API,
  • optionally, the active tray is set in Spoolman Updater,
  • and a notification is sent to mobile_app_julians_smartphone (which spool / new storage location).

3) Naming Storage Locations in Spoolman

  • Connect words with hyphens.
  • No special characters, no umlauts (e.g., instead of “Tür” use “tuer”).
  • Use consistent spelling (e.g., all lowercase, 3d-printer-side instead of 3D printer side).
  • IMPORTANT: The AMS must be named in Spoolman as a location like this:
    • Example: p1s-01p00a9b9000999-ams-1
      • If there are multiple AMS units, simply adjust the number after “ams”, examples:
        • p1s-01p00a9b9000999-ams-2
        • p1s-01p00a9b9000999-ams-3
        • p1s-01p00a9b9000999-ams-4
  • IMPORTANT: The external spool must be named in Spoolman as a location like this:
    • Example: p1s_01p00a9b9000999_externalspool_externe_spule
  • The exact naming of the respective entities must be checked in Home Assistant, as it may differ depending on language!

4) Helpers in Home Assistant

The automation uses input_text helpers to temporarily store the last scanned spool/location/tray. You need:

  • input_text.spoolman_last_spool - stores the last scanned spool tag
  • input_text.spoolman_last_location - stores the last scanned location tag
  • input_text.spoolman_last_tray - stores the last scanned tray tag

These helpers are simple text fields. After a successful assignment, the automation clears them again.

5) Tags - Format & Examples

So Home Assistant can decide whether a tag represents a spool, a location, or a tray based on the tag content, tags must be created using fixed naming conventions:

  • Location tag starts with web+spoolman:loc-
    Example: web+spoolman:loc-3d-printer-side
  • Spool tag starts with web+spoolman:s-
    Example: web+spoolman:s-37
  • Tray tag starts with web+spoolman:tray:
    Example: web+spoolman:tray:p1s_01p00x3b2000xxx_ams_1_slot_4

How to create tags in Home Assistant

In Home Assistant, you can create new tags under Settings → Tags and assign exactly these strings as the Tag ID. As the physical medium you can:

  • generate/print QR codes
  • write NFC tags

6) APIs - How Communication Works

Bambu Lab

The Bambu Lab integration provides the entities required for Spoolman Updater:

  • Entities from one or more AMS units for the respective trays/slots
  • Entity for the external spool

Spoolman API

Spoolman provides a REST API. To set the location, a PATCH request is used on the spool endpoint:

  • Endpoint: /api/v1/spool/<spool_id>
  • Payload: {"location":"<location>"}

Spoolman Updater API

Spoolman Updater provides endpoints to update spool data and to assign a tray ID. In this configuration:

  • Spool data is updated via POST to /Spools (e.g., active_tray_id, tag UID, etc.).
  • Tray assignment is set via POST to /Spools/tray.

7) Home Assistant Configuration (REST + REST Commands)

The domain below is intentionally generic. Replace:

  • https://spoolman.example.local with your Spoolman URL
  • https://spoolman-updater.example.local with your Spoolman Updater URL

configuration.yaml

rest: !include_dir_merge_list apis/
rest_command: !include_dir_merge_named rest_commands/

/homeassistant/rest_commands/spoolman.yaml

  update_spool:
    url: "https://spoolman-updater.example.local/Spools"
    method: POST
    headers:
      Content-Type: "application/json"
    payload: >
      {
        "name": "{{ filament_name }}",
        "material": "{{ filament_material }}",
        "tag_uid": "{{ filament_tag_uid }}",
        "used_weight": {{ filament_used_weight | int(0) }},
        "color": "{{ filament_color }}",
        "active_tray_id": "{{ filament_active_tray_id }}"
      }

  spoolman_set_location:
    url: "https://spoolman.example.local/api/v1/spool/{{ spool_id }}"
    method: PATCH
    headers:
      Content-Type: "application/json"
    payload: >
      {"location":"{{ location }}"}

  spoolman_updater_set_tray:
    url: "https://spoolman-updater.example.local/Spools/tray"
    method: POST
    headers:
      Content-Type: "application/json"
    payload: >
      {
        "spool_id": {{ spool_id | int(0) }},
        "active_tray_id": "{{ active_tray_id }}"
      }

/homeassistant/apis/spoolman.yaml

This REST sensor setup is used to load the last scanned spool from Spoolman (e.g., for displaying it on dashboards). With scan_interval: 0, it only updates when Home Assistant explicitly triggers a refresh (not periodically).

- resource: "https://spoolman.example.local/api/v1/spool/{{ states('input_text.spoolman_last_spool') | replace('web+spoolman:s-','') }}"
  scan_interval: 0
  sensor:
    - name: spoolman_live_spool
      value_template: "{{ value_json.id }}"
      json_attributes:
        - id
        - used_weight
        - filament
        - location

8) Notification (Companion App)

The automation sends a push message via:

  • notify.mobile_app_julians_smartphone

The message content can include, for example:

  • Filament spool: ID - Friendly Name
  • Storage location: Location name

The ID is extracted from the entity ID or the tag schema (e.g., web+spoolman:s-37 → ID 37). The friendly name typically comes from the Spoolman entity and/or its attributes. The new location comes directly from the scanned location tag or is derived from the tray tag.

9) Tips & Notes

  • Consistent naming: Create locations in Spoolman so they can be used 1:1 as the tag suffix (e.g., lowercase + hyphens).
  • Place tags sensibly: Location tags on the shelf/box, spool tags directly on the spool, tray tags on the printer/AMS.
  • Error cases: If only the spool or only the location/tray is scanned, the value remains stored in the helper until the counterpart is scanned.

10) Automation

alias: Spoolman - Assign spool to Location/Tray via NFC/QR
description: >-
  This automation assigns Spoolman filament spools to a storage location or to a
  tray based on NFC/QR tag scans and then sends a push notification about the
  change.

  It is event-based (tag_scanned) and does not require fixed spool IDs, so it
  works for any number of spools.

  Tag conventions (recognized by fixed prefixes), examples: - Location (e.g. "3D
  printer side"):
    web+spoolman:loc-3d-printer-side
  - Spool #37:
    web+spoolman:s-37
  - Tray (e.g. "P1S #1 – AMS #1 – Slot #4"):
    web+spoolman:tray:p1s_01p00a9b9000999_ams_1_slot_4
  - External Spool (e.g. "P1S #1 – External Spool"):
    web+spoolman:tray:p1s_01p00a9b9000999_externalspool_externe_spule

  Once a spool AND a location OR tray (incl. external spool) have been scanned,
  Spoolman is updated. For tray/external spool it also forwards the tray info to
  Spoolman Updater. Finally, a notification with spool ID, spool name and new
  location is sent.
triggers:
  - event_type: tag_scanned
    trigger: event
actions:
  - alias: Store last scanned tag (spool/location/tray)
    choose:
      - alias: Tag is a spool -> store spool tag
        conditions:
          - condition: template
            value_template: "{{ is_spool }}"
        sequence:
          - alias: Save spool tag to input_text.spoolman_last_spool
            action: input_text.set_value
            target:
              entity_id: input_text.spoolman_last_spool
            data:
              value: "{{ tag_id }}"
      - alias: Tag is a location -> store location tag
        conditions:
          - condition: template
            value_template: "{{ is_loc }}"
        sequence:
          - alias: Save location tag to input_text.spoolman_last_location
            action: input_text.set_value
            target:
              entity_id: input_text.spoolman_last_location
            data:
              value: "{{ tag_id }}"
      - alias: Tag is a tray -> store tray tag
        conditions:
          - condition: template
            value_template: "{{ is_tray }}"
        sequence:
          - alias: Save tray tag to input_text.spoolman_last_tray
            action: input_text.set_value
            target:
              entity_id: input_text.spoolman_last_tray
            data:
              value: "{{ tag_id }}"
  - alias: Compute spool/location/tray variables
    variables:
      spool_tag: "{{ states('input_text.spoolman_last_spool') }}"
      loc_tag: "{{ states('input_text.spoolman_last_location') }}"
      tray_tag: "{{ states('input_text.spoolman_last_tray') }}"
      spool_id: "{{ spool_tag | replace('web+spoolman:s-','') | int(0) }}"
      location_from_loc: "{{ loc_tag | replace('web+spoolman:loc-','') }}"
      active_tray_id: "{{ tray_tag | replace('web+spoolman:tray:','') }}"
      is_external_spool: "{{ '_externalspool_' in active_tray_id }}"
      location_from_tray: |-
        {{
          active_tray_id
            | regex_replace('_slot_\\d+$', '')
            | replace('_','-')
        }}
      spool_location_entity: select.spoolman_spool_{{ spool_id }}_location
      spool_location_friendly_raw: >-
        {{ state_attr(spool_location_entity, 'friendly_name') | default('',
        true) }}
      spool_friendly: |-
        {{
          spool_location_friendly_raw
            | regex_replace('\\s*[Ll]ocation\\s*$', '')
            | trim
        }}
  - alias: Assign spool to a Spoolman location (spool + location scanned)
    choose:
      - alias: "Condition: spool_id valid AND location tag present"
        conditions:
          - condition: template
            value_template: "{{ spool_id != 0 and location_from_loc != '' }}"
        sequence:
          - alias: "REST: Set Spoolman location from location tag"
            action: rest_command.spoolman_set_location
            data:
              spool_id: "{{ spool_id }}"
              location: "{{ location_from_loc }}"
          - alias: "Notify: Location changed (location tag)"
            action: notify.mobile_app_julians_smartphone
            data:
              title: "Spoolman: Location updated"
              message: >-
                #{{ spool_id }} - {{ spool_friendly if spool_friendly else
                'Unknown' }}

                -

                {{ location_from_loc }}
              data:
                tag: spoolman_location_{{ spool_id }}
          - alias: "Cleanup: Clear last spool tag"
            action: input_text.set_value
            target:
              entity_id: input_text.spoolman_last_spool
            data:
              value: ""
          - alias: "Cleanup: Clear last location tag"
            action: input_text.set_value
            target:
              entity_id: input_text.spoolman_last_location
            data:
              value: ""
  - alias: Assign spool to a tray/external spool (spool + tray scanned)
    choose:
      - alias: "Condition: spool_id valid AND tray tag present"
        conditions:
          - condition: template
            value_template: "{{ spool_id != 0 and active_tray_id != '' }}"
        sequence:
          - alias: "REST: Set Spoolman location derived from tray tag"
            action: rest_command.spoolman_set_location
            data:
              spool_id: "{{ spool_id }}"
              location: "{{ location_from_tray }}"
          - alias: "REST: Forward tray/external spool to Spoolman Updater"
            action: rest_command.spoolman_updater_set_tray
            data:
              spool_id: "{{ spool_id }}"
              active_tray_id: "{{ active_tray_id }}"
          - alias: "Notify: Location changed (tray/external spool)"
            action: notify.mobile_app_julians_smartphone
            data:
              title: "Spoolman: Location updated"
              message: >-
                #{{ spool_id }} - {{ spool_friendly if spool_friendly else
                'Unknown' }}

                -

                {{ location_from_tray }}
              data:
                tag: spoolman_location_{{ spool_id }}
          - alias: "Cleanup: Clear last spool tag"
            action: input_text.set_value
            target:
              entity_id: input_text.spoolman_last_spool
            data:
              value: ""
          - alias: "Cleanup: Clear last tray tag"
            action: input_text.set_value
            target:
              entity_id: input_text.spoolman_last_tray
            data:
              value: ""
variables:
  tag_id: "{{ trigger.event.data.tag_id }}"
  is_spool: "{{ tag_id.startswith('web+spoolman:s-') }}"
  is_loc: "{{ tag_id.startswith('web+spoolman:loc-') }}"
  is_tray: "{{ tag_id.startswith('web+spoolman:tray:') }}"
mode: restart
2 Likes

For the Spoolman Updater, I used the following Docker Compose configuration:

services:
  spoolman-updater:
    image: marcokreeft/spoolman-updater:latest
    container_name: spoolman-updater
    ports:
      - "8088:8080"
    environment:
      - APPLICATION__HOMEASSISTANT__URL=http://my-home-assistant-instance.local:8123
      - APPLICATION__HOMEASSISTANT__TOKEN=my-api-token.from-home-assistant
      - APPLICATION__SPOOLMAN__URL=http://my-spoolman-instance.local:8088
      - APPLICATION__HOMEASSISTANT__AMSENTITIES__0=P1S_01P00A9B9000999_AMS_1
      - APPLICATION__HOMEASSISTANT__EXTERNALSPOOLENTITY=sensor.p1s_01p00a9b9000999_externalspool_externe_spule
    restart: unless-stopped