Skip to content

bambustate

bambustate provides a unified, thread-safe representation of a Bambu Lab printer's operational state, synchronized via MQTT telemetry.

Classes:

Name Description
AMSUnitState

State information about an individual AMS unit.

BambuClimate

Contains all climate related attributes

BambuState

Representation of the Bambu printer state synchronized via MQTT.

ExtruderState

State for an individual physical extruder toolhead.

AMSUnitState dataclass

Python
AMSUnitState(
    ams_id: int,
    chip_id: str = "",
    model: AMSModel = UNKNOWN,
    temp_actual: float = 0.0,
    temp_target: int = 0,
    humidity_index: int = 0,
    humidity_raw: int = 0,
    ams_info: int = 0,
    heater_state: AMSHeatingState = NO_POWER,
    raw_extruder_id: int = -1,
    dry_time: int = 0,
    tray_exists: list[bool] = (lambda: [False] * 4)(),
    assigned_to_extruder: ActiveTool = SINGLE_EXTRUDER,
)

State information about an individual AMS unit.

Attributes:

Name Type Description
ams_id int

Unique ID. Population: str(ams_idx).

ams_info int

Underlying ams info value

assigned_to_extruder ActiveTool

Target tool computed from raw_extruder_id

chip_id str

Hardware serial. Population: m.get("sn").

dry_time int

Minutes left. Population: int(float(r.get("dry_time"))).

heater_state AMSHeatingState

The computed state of the AMS's heater

humidity_index int

Humidity index. Population: int(float(r.get("humidity"))).

humidity_raw int

Raw humidity. Population: int(float(r.get("humidity_raw"))).

model AMSModel

AMSModel for this unit

raw_extruder_id int

Raw extruder ID extracted from ams_info

temp_actual float

Actual temp. Population: float(r.get("temp")).

temp_target int

Target drying temp. Population: trays[0].drying_temp.

tray_exists list[bool]

Slot presence. Population: Shifting tray_exist_bits.

ams_id instance-attribute

Python
ams_id: int

Unique ID. Population: str(ams_idx).

ams_info class-attribute instance-attribute

Python
ams_info: int = 0

Underlying ams info value

assigned_to_extruder class-attribute instance-attribute

Python
assigned_to_extruder: ActiveTool = SINGLE_EXTRUDER

Target tool computed from raw_extruder_id

chip_id class-attribute instance-attribute

Python
chip_id: str = ''

Hardware serial. Population: m.get("sn").

dry_time class-attribute instance-attribute

Python
dry_time: int = 0

Minutes left. Population: int(float(r.get("dry_time"))).

heater_state class-attribute instance-attribute

Python
heater_state: AMSHeatingState = NO_POWER

The computed state of the AMS's heater

humidity_index class-attribute instance-attribute

Python
humidity_index: int = 0

Humidity index. Population: int(float(r.get("humidity"))).

humidity_raw class-attribute instance-attribute

Python
humidity_raw: int = 0

Raw humidity. Population: int(float(r.get("humidity_raw"))).

model class-attribute instance-attribute

Python
model: AMSModel = UNKNOWN

AMSModel for this unit

raw_extruder_id class-attribute instance-attribute

Python
raw_extruder_id: int = -1

Raw extruder ID extracted from ams_info

temp_actual class-attribute instance-attribute

Python
temp_actual: float = 0.0

Actual temp. Population: float(r.get("temp")).

temp_target class-attribute instance-attribute

Python
temp_target: int = 0

Target drying temp. Population: trays[0].drying_temp.

tray_exists class-attribute instance-attribute

Python
tray_exists: list[bool] = field(default_factory=lambda: [False] * 4)

Slot presence. Population: Shifting tray_exist_bits.

BambuClimate dataclass

Python
BambuClimate(
    bed_temp: float = 0.0,
    bed_temp_target: int = 0,
    airduct_mode: int = -1,
    airduct_sub_mode: int = -1,
    chamber_temp: float = 0.0,
    chamber_temp_target: int = 0,
    air_conditioning_mode: AirConditioningMode = NOT_SUPPORTED,
    part_cooling_fan_speed_percent: int = 0,
    part_cooling_fan_speed_target_percent: int = 0,
    aux_fan_speed_percent: int = 0,
    exhaust_fan_speed_percent: int = 0,
    heatbreak_fan_speed_percent: int = 0,
    zone_intake_open: bool = False,
    zone_part_fan_percent: int = 0,
    zone_aux_percent: int = 0,
    zone_exhaust_percent: int = 0,
    zone_top_vent_open: bool = False,
    is_chamber_door_open: bool = False,
    is_chamber_lid_open: bool = False,
)

Contains all climate related attributes

Attributes:

Name Type Description
air_conditioning_mode AirConditioningMode

The mode the printer's AC is in if equipped with one.

airduct_mode int

Raw current mode. Population: airduct.modeCur.

airduct_sub_mode int

Raw sub mode. Population: airduct.subMode.

aux_fan_speed_percent int

aux fan %. Population: scaleFanSpeed(p.big_fan1_speed).

bed_temp float

Bed temp. Population: float(p.get("bed_temper")).

bed_temp_target int

Bed target. Population: float(p.get("bed_target_temper")).

chamber_temp float

Chamber temp. Population: unpackTemperature(ctc_root.info.temp).

chamber_temp_target int

Chamber target. Population: unpackTemperature(ctc_root.info.temp).

exhaust_fan_speed_percent int

Exhaust (chamber) fan %. Population: scaleFanSpeed(p.big_fan2_speed).

heatbreak_fan_speed_percent int

Heatbreak fan %. Population: scaleFanSpeed(p.heatbreak_fan_speed).

is_chamber_door_open bool

For printers that support it (see PrinterCapabilities.has_chamber_door_sensor), reports whether the chamber door is open

is_chamber_lid_open bool

For printers that support it (see PrinterCapabilities.has_chamber_door_sensor), reports whether the chamber lid is open

part_cooling_fan_speed_percent int

Part fan %. Population: scaleFanSpeed(p.cooling_fan_speed).

part_cooling_fan_speed_target_percent int

Part target %. Population: scaleFanSpeed(p.cooling_fan_target_speed).

zone_aux_percent int

aux %. Population: airduct.parts ID 32.

zone_exhaust_percent int

Exhaust %. Population: airduct.parts ID 48.

zone_intake_open bool

Heater power. Population: airduct.parts ID 96.

zone_part_fan_percent int

Internal %. Population: airduct.parts ID 16.

zone_top_vent_open bool

Top vent status - derived from exhaust fan on and cooling ac mode.

air_conditioning_mode class-attribute instance-attribute

Python
air_conditioning_mode: AirConditioningMode = NOT_SUPPORTED

The mode the printer's AC is in if equipped with one.

airduct_mode class-attribute instance-attribute

Python
airduct_mode: int = -1

Raw current mode. Population: airduct.modeCur.

airduct_sub_mode class-attribute instance-attribute

Python
airduct_sub_mode: int = -1

Raw sub mode. Population: airduct.subMode.

aux_fan_speed_percent class-attribute instance-attribute

Python
aux_fan_speed_percent: int = 0

aux fan %. Population: scaleFanSpeed(p.big_fan1_speed).

bed_temp class-attribute instance-attribute

Python
bed_temp: float = 0.0

Bed temp. Population: float(p.get("bed_temper")).

bed_temp_target class-attribute instance-attribute

Python
bed_temp_target: int = 0

Bed target. Population: float(p.get("bed_target_temper")).

chamber_temp class-attribute instance-attribute

Python
chamber_temp: float = 0.0

Chamber temp. Population: unpackTemperature(ctc_root.info.temp).

chamber_temp_target class-attribute instance-attribute

Python
chamber_temp_target: int = 0

Chamber target. Population: unpackTemperature(ctc_root.info.temp).

exhaust_fan_speed_percent class-attribute instance-attribute

Python
exhaust_fan_speed_percent: int = 0

Exhaust (chamber) fan %. Population: scaleFanSpeed(p.big_fan2_speed).

heatbreak_fan_speed_percent class-attribute instance-attribute

Python
heatbreak_fan_speed_percent: int = 0

Heatbreak fan %. Population: scaleFanSpeed(p.heatbreak_fan_speed).

is_chamber_door_open class-attribute instance-attribute

Python
is_chamber_door_open: bool = False

For printers that support it (see PrinterCapabilities.has_chamber_door_sensor), reports whether the chamber door is open

is_chamber_lid_open class-attribute instance-attribute

Python
is_chamber_lid_open: bool = False

For printers that support it (see PrinterCapabilities.has_chamber_door_sensor), reports whether the chamber lid is open

part_cooling_fan_speed_percent class-attribute instance-attribute

Python
part_cooling_fan_speed_percent: int = 0

Part fan %. Population: scaleFanSpeed(p.cooling_fan_speed).

part_cooling_fan_speed_target_percent class-attribute instance-attribute

Python
part_cooling_fan_speed_target_percent: int = 0

Part target %. Population: scaleFanSpeed(p.cooling_fan_target_speed).

zone_aux_percent class-attribute instance-attribute

Python
zone_aux_percent: int = 0

aux %. Population: airduct.parts ID 32.

zone_exhaust_percent class-attribute instance-attribute

Python
zone_exhaust_percent: int = 0

Exhaust %. Population: airduct.parts ID 48.

zone_intake_open class-attribute instance-attribute

Python
zone_intake_open: bool = False

Heater power. Population: airduct.parts ID 96.

zone_part_fan_percent class-attribute instance-attribute

Python
zone_part_fan_percent: int = 0

Internal %. Population: airduct.parts ID 16.

zone_top_vent_open class-attribute instance-attribute

Python
zone_top_vent_open: bool = False

Top vent status - derived from exhaust fan on and cooling ac mode.

BambuState dataclass

Python
BambuState(
    gcode_state: str = "IDLE",
    current_stage_id: int = 0,
    current_stage_name: str = "",
    print_percentage: int = 0,
    monotonic_start_time: int = -1,
    elapsed_minutes: int = 0,
    remaining_minutes: float = 0.0,
    current_layer: int = 0,
    total_layers: int = 0,
    active_ams_id: int = -1,
    active_tray_id: int = 255,
    active_tray_state: TrayState = UNLOADED,
    active_tray_state_name: str = name,
    target_tray_id: int = -1,
    active_tool: ActiveTool = SINGLE_EXTRUDER,
    is_external_spool_active: bool = False,
    active_nozzle_temp: float = 0.0,
    active_nozzle_temp_target: int = 0,
    ams_status_raw: int = 0,
    ams_status_text: str = "",
    ams_exist_bits: int = 0,
    ams_connected_count: int = 0,
    ams_units: list[AMSUnitState] = list(),
    extruders: list[ExtruderState] = list(),
    spools: list[BambuSpool] = list(),
    print_error: int = 0,
    hms_errors: list[dict] = list(),
    wifi_signal_strength: str = "",
    climate: BambuClimate = BambuClimate(),
    stat: str = "0",
    fun: str = "0",
)

Representation of the Bambu printer state synchronized via MQTT.

Methods:

Name Description
fromJson

Parses root MQTT payloads into a hierachical BambuState object.

Attributes:

Name Type Description
active_ams_id int

Current active AMS unit id

active_nozzle_temp float

Nozzle temp. Population: Handoff from a_ext or p.

active_nozzle_temp_target int

Nozzle target. Population: Handoff from a_ext or p.

active_tool ActiveTool

Active toolhead. Population: extruder_root.state shift.

active_tray_id int

Current tray. Population: Computed in Tool Handoff.

active_tray_state TrayState

Loading enum. Population: ExtruderInfoState check.

active_tray_state_name str

Loading string. Population: active_tray_state.name.

ams_connected_count int

AMS count. Population: bin(ams_exist_bits).count("1").

ams_exist_bits int

AMS mask. Population: int(ams_root.ams_exist_bits, 16).

ams_status_raw int

Raw AMS status. Population: int(p.get("ams_status")).

ams_status_text str

Human AMS status. Population: parseAMSStatus.

ams_units list[AMSUnitState]

Unit details. Population: Result of unit iteration.

climate BambuClimate

Contains all climate related attributes

current_layer int

Layer index. Population: int(p.get("layer_num")).

current_stage_id int

Stage numeric ID. Population: int(p.get("stg_cur")).

current_stage_name str

Stage human name. Population: parseStage.

elapsed_minutes int

The elapsed time in minutes for this (or the last) job

extruders list[ExtruderState]

Extruder details. Population: Result of extruder iteration.

gcode_state str

Execution state. Population: p.get("gcode_state").

hms_errors list[dict]

HMS list. Population: decodeHMS + decodeError synthesis.

is_external_spool_active bool

Ext spool flag. Population: active_tray_id in [254, 255].

monotonic_start_time int

The monotonic time stamp of when this job started

print_error int

Main error. Population: int(p.get("print_error")).

print_percentage int

Completion %. Population: int(p.get("mc_percent")).

remaining_minutes float

Time remaining in minutes for the current job. Population: int(p.get("mc_remaining_time")).

spools list[BambuSpool]

All spools associated with this printer

target_tray_id int

Next tray. Population: Stage-specific targeting logic.

total_layers int

Layer total. Population: int(p.get("total_layer_num")).

wifi_signal_strength str

Wi-Fi signal strength in dBm

active_ams_id class-attribute instance-attribute

Python
active_ams_id: int = -1

Current active AMS unit id

active_nozzle_temp class-attribute instance-attribute

Python
active_nozzle_temp: float = 0.0

Nozzle temp. Population: Handoff from a_ext or p.

active_nozzle_temp_target class-attribute instance-attribute

Python
active_nozzle_temp_target: int = 0

Nozzle target. Population: Handoff from a_ext or p.

active_tool class-attribute instance-attribute

Python
active_tool: ActiveTool = SINGLE_EXTRUDER

Active toolhead. Population: extruder_root.state shift.

active_tray_id class-attribute instance-attribute

Python
active_tray_id: int = 255

Current tray. Population: Computed in Tool Handoff.

active_tray_state class-attribute instance-attribute

Python
active_tray_state: TrayState = UNLOADED

Loading enum. Population: ExtruderInfoState check.

active_tray_state_name class-attribute instance-attribute

Python
active_tray_state_name: str = name

Loading string. Population: active_tray_state.name.

ams_connected_count class-attribute instance-attribute

Python
ams_connected_count: int = 0

AMS count. Population: bin(ams_exist_bits).count("1").

ams_exist_bits class-attribute instance-attribute

Python
ams_exist_bits: int = 0

AMS mask. Population: int(ams_root.ams_exist_bits, 16).

ams_status_raw class-attribute instance-attribute

Python
ams_status_raw: int = 0

Raw AMS status. Population: int(p.get("ams_status")).

ams_status_text class-attribute instance-attribute

Python
ams_status_text: str = ''

Human AMS status. Population: parseAMSStatus.

ams_units class-attribute instance-attribute

Python
ams_units: list[AMSUnitState] = field(default_factory=list)

Unit details. Population: Result of unit iteration.

climate class-attribute instance-attribute

Python
climate: BambuClimate = field(default_factory=BambuClimate)

Contains all climate related attributes

current_layer class-attribute instance-attribute

Python
current_layer: int = 0

Layer index. Population: int(p.get("layer_num")).

current_stage_id class-attribute instance-attribute

Python
current_stage_id: int = 0

Stage numeric ID. Population: int(p.get("stg_cur")).

current_stage_name class-attribute instance-attribute

Python
current_stage_name: str = ''

Stage human name. Population: parseStage.

elapsed_minutes class-attribute instance-attribute

Python
elapsed_minutes: int = 0

The elapsed time in minutes for this (or the last) job

extruders class-attribute instance-attribute

Python
extruders: list[ExtruderState] = field(default_factory=list)

Extruder details. Population: Result of extruder iteration.

gcode_state class-attribute instance-attribute

Python
gcode_state: str = 'IDLE'

Execution state. Population: p.get("gcode_state").

hms_errors class-attribute instance-attribute

Python
hms_errors: list[dict] = field(default_factory=list)

HMS list. Population: decodeHMS + decodeError synthesis.

is_external_spool_active class-attribute instance-attribute

Python
is_external_spool_active: bool = False

Ext spool flag. Population: active_tray_id in [254, 255].

monotonic_start_time class-attribute instance-attribute

Python
monotonic_start_time: int = -1

The monotonic time stamp of when this job started

print_error class-attribute instance-attribute

Python
print_error: int = 0

Main error. Population: int(p.get("print_error")).

print_percentage class-attribute instance-attribute

Python
print_percentage: int = 0

Completion %. Population: int(p.get("mc_percent")).

remaining_minutes class-attribute instance-attribute

Python
remaining_minutes: float = 0.0

Time remaining in minutes for the current job. Population: int(p.get("mc_remaining_time")).

spools class-attribute instance-attribute

Python
spools: list[BambuSpool] = field(default_factory=list)

All spools associated with this printer

target_tray_id class-attribute instance-attribute

Python
target_tray_id: int = -1

Next tray. Population: Stage-specific targeting logic.

total_layers class-attribute instance-attribute

Python
total_layers: int = 0

Layer total. Population: int(p.get("total_layer_num")).

wifi_signal_strength class-attribute instance-attribute

Python
wifi_signal_strength: str = ''

Wi-Fi signal strength in dBm

fromJson classmethod

Python
fromJson(
    data: dict[str, Any], current_state: BambuState, config: BambuConfig
) -> BambuState

Parses root MQTT payloads into a hierachical BambuState object.

Source code in src/bpm/bambustate.py
Python
@classmethod
def fromJson(
    cls, data: dict[str, Any], current_state: "BambuState", config: BambuConfig
) -> "BambuState":
    """Parses root MQTT payloads into a hierachical BambuState object."""

    base = current_state if current_state else cls()
    info = data.get("info", {})
    p = data.get("print", {})

    if (
        p.get("command", "") == "ams_filament_drying"
        and p.get("result", "") == "success"
    ):
        ams_id = p.get("ams_id", -1)
        ams = next((u for u in base.ams_units if u.ams_id == ams_id), None)
        if ams:
            ams.temp_target = int(p.get("temp", 0))

    ams_root = p.get("ams", {})
    device = p.get("device", {})

    extruder_root = device.get("extruder", {})
    ctc_root = device.get("ctc", {})
    airduct_root = device.get("airduct", {})

    modules = info.get("module", [])
    updates = {}

    # CAPABILITIES
    caps = asdict(config.capabilities)
    if ctc_root:
        caps["has_chamber_temp"] = True
    if "ams" in ams_root or "ams" in p:
        caps["has_ams"] = True
    if airduct_root:
        caps["has_air_filtration"] = True
    if len(extruder_root.get("info", [])) > 1:
        caps["has_dual_extruder"] = True

    caps["has_camera"] = True

    xcam_data = p.get("xcam", None)
    if xcam_data:
        caps["has_lidar"] = xcam_data.get("first_layer_inspector", False)
    else:
        caps["has_lidar"] = config.capabilities.has_lidar

    new_caps = PrinterCapabilities(**caps)

    climate = asdict(base.climate)
    updates["climate"] = BambuClimate(**climate)

    # STATUS & PROGRESS
    updates["gcode_state"] = p.get("gcode_state", base.gcode_state)
    updates["current_stage_id"] = int(p.get("stg_cur", base.current_stage_id))
    updates["current_stage_name"] = parseStage(updates["current_stage_id"])

    updates["fun"] = p.get("fun", base.fun)
    fun = int(updates["fun"], 16)
    new_caps.has_chamber_door_sensor = bool((fun >> 12) & 0x01)

    if new_caps.has_chamber_door_sensor:
        updates["stat"] = p.get("stat", base.stat)
        stat = int(updates["stat"], 16)
        updates["climate"].is_chamber_door_open = bool((stat >> 23) & 0x01)
        updates["climate"].is_chamber_lid_open = bool((stat >> 24) & 0x01)

    if (
        updates["gcode_state"] in ("FAILED", "FINISH")
        and updates["gcode_state"] != base.gcode_state
    ):
        updates["monotonic_start_time"] = -1
    elif (
        updates["gcode_state"] in ("PREPARE", "RUNNING")
        and base.monotonic_start_time == -1
    ):
        updates["monotonic_start_time"] = time.monotonic()
    else:
        if updates["gcode_state"] in ("PREPARE", "RUNNING"):
            updates["elapsed_minutes"] = (
                time.monotonic()
                - updates.get("monotonic_start_time", base.monotonic_start_time)
            ) / 60.0

            updates["current_layer"] = int(p.get("layer_num", base.current_layer))
            updates["print_percentage"] = int(
                p.get("mc_percent", base.print_percentage)
            )
            updates["total_layers"] = int(p.get("total_layer_num", base.total_layers))
            updates["remaining_minutes"] = float(
                p.get("mc_remaining_time", base.remaining_minutes)
            )

    # AIRDUCT
    if airduct_root:
        updates["climate"].airduct_mode = int(
            airduct_root.get("modeCur", base.climate.airduct_mode)
        )
        updates["climate"].airduct_sub_mode = int(
            airduct_root.get("subMode", base.climate.airduct_sub_mode)
        )

        if updates["climate"].airduct_mode == 1:
            updates["climate"].air_conditioning_mode = AirConditioningMode.HEAT_MODE
        elif updates["climate"].airduct_mode == 0:
            updates["climate"].air_conditioning_mode = AirConditioningMode.COOL_MODE
            base.climate.chamber_temp_target = 0
        else:
            updates[
                "climate"
            ].air_conditioning_mode = AirConditioningMode.NOT_SUPPORTED

        parts = {part["id"]: part["state"] for part in airduct_root.get("parts", [])}

        updates["climate"].zone_part_fan_percent = parts.get(
            16, base.climate.zone_part_fan_percent
        )
        updates["climate"].zone_aux_percent = parts.get(
            32, base.climate.zone_aux_percent
        )
        updates["climate"].zone_exhaust_percent = parts.get(
            48, base.climate.zone_exhaust_percent
        )

        zone_intake_open = parts.get(96, -1)
        updates["climate"].zone_intake_open = zone_intake_open not in (-1, 0)

        updates["climate"].zone_top_vent_open = bool(
            updates["climate"].zone_exhaust_percent > 0
            and not updates["climate"].zone_intake_open
        )
        # bit 58 apparently has the top vent value
        # is_top_vent_closed_bit = (fun >> 58) & 1
        # print(f"\r\ntop bit=[{is_top_vent_closed_bit != 1}] update=[{updates['climate'].zone_top_vent_open}]\r\n")
        # if (not is_top_vent_closed_bit) != updates["climate"].zone_top_vent_open:
        #     updates["climate"].zone_top_vent_open = not is_top_vent_closed_bit

    # THERMALS & CTC DECODING
    updates["climate"].bed_temp = float(p.get("bed_temper", base.climate.bed_temp))
    updates["climate"].bed_temp_target = int(
        p.get("bed_target_temper", base.climate.bed_temp_target)
    )

    ctc_temp_target = 0
    if ctc_root:
        ctc_temp_raw = unpackTemperature(ctc_root.get("info", {}).get("temp", 0.0))
        ctc_temp = ctc_temp_raw[0]
        ctc_temp_target = int(ctc_temp_raw[1])
        updates["climate"].chamber_temp = ctc_temp
        updates["climate"].chamber_temp_target = ctc_temp_target
    else:
        updates["climate"].chamber_temp = p.get(
            "chamber_temper", base.climate.chamber_temp
        )

    if (
        ctc_temp_target == 0
        and updates["climate"].air_conditioning_mode != AirConditioningMode.HEAT_MODE
    ):
        updates["climate"].chamber_temp_target = base.climate.chamber_temp_target

    if p.get("command", "") == "set_ctt" and p.get("result", "") == "success":
        ctc_temp_target = int(p.get("ctt_val", -1))
        updates["climate"].chamber_temp_target = ctc_temp_target
        if ctc_temp_target < 45:
            updates["climate"].air_conditioning_mode = AirConditioningMode.COOL_MODE

    # EXTRUDERS
    new_extruders = []
    if "info" in extruder_root:
        for new_ext in extruder_root["info"]:
            raw_t = int(new_ext.get("temp", 0))
            act_t, tar_t = unpackTemperature(raw_t)

            sn = int(new_ext.get("snow", -1))
            hn = int(new_ext.get("hnow", -1))

            st = int(new_ext.get("star", -1))
            ht = int(new_ext.get("htar", -1))

            ext = ExtruderState()
            ext.id = int(new_ext.get("id", 0))

            ext.temp = act_t
            ext.temp_target = int(tar_t)

            ext.info_bits = int(new_ext.get("info", 0))
            ext.state = parseExtruderInfo(ext.info_bits)
            ext.status = parseExtruderStatus(int(new_ext.get("stat", 0)))

            ext.active_tray_id = parseExtruderTrayState(ext.id, hn, sn)
            ext.target_tray_id = parseExtruderTrayState(ext.id, ht, st)

            if base.extruders and len(base.extruders) > ext.id:
                base_tray_state = base.extruders[ext.id].tray_state
            else:
                base_tray_state = (
                    TrayState.LOADED
                    if ext.state != ExtruderInfoState.EMPTY
                    else TrayState.UNLOADED
                )

            if (
                ext.state == ExtruderInfoState.LOADED
                and ext.status == ExtruderStatus.ACTIVE
            ):
                ext.tray_state = TrayState.LOADED
            elif (
                ext.state == ExtruderInfoState.EMPTY
                and ext.status == ExtruderStatus.IDLE
            ):
                ext.tray_state = TrayState.UNLOADED
            elif (
                ext.state == ExtruderInfoState.LOADED
                and ext.status == ExtruderStatus.HEATING
                and base_tray_state not in (TrayState.LOADING, TrayState.UNLOADED)
            ):
                ext.tray_state = TrayState.UNLOADING
            elif ext.status is not ExtruderStatus.IDLE:
                ext.tray_state = TrayState.LOADING
            else:
                ext.tray_state = base.active_tray_state

            new_extruders.append(ext)
    updates["extruders"] = new_extruders if new_extruders else base.extruders

    # TOOL SELECTION
    if "state" in extruder_root:
        raw_t_idx = (int(extruder_root["state"]) >> 4) & 0xF
        if new_caps.has_dual_extruder:
            updates["active_tool"] = ActiveTool(raw_t_idx)
        else:
            updates["active_tool"] = ActiveTool.SINGLE_EXTRUDER
    else:
        updates["active_tool"] = base.active_tool

    # AMS UNITS
    cur_ams = {u.ams_id: u for u in base.ams_units}

    for m in modules:
        if (
            m.get("name", "").startswith("n3f/")
            or m.get("name", "").startswith("n3s/")
            or m.get("name", "").startswith("ams")
        ):
            ams_id = int(m["name"].split("/")[-1])
            u = cur_ams.get(ams_id, AMSUnitState(ams_id=ams_id))
            u.chip_id = m.get("sn", u.chip_id)
            u.model = getAMSModelBySerial(u.chip_id)
            cur_ams[ams_id] = u

    for ams_u in ams_root.get("ams", []):
        id = int(ams_u.get("id", 0))
        u = cur_ams.get(id, AMSUnitState(ams_id=id))
        u.temp_actual = float(ams_u.get("temp", u.temp_actual))
        u.humidity_index = int(float(ams_u.get("humidity", u.humidity_index)))
        u.humidity_raw = int(float(ams_u.get("humidity_raw", u.humidity_raw)))
        u.dry_time = int(float(ams_u.get("dry_time", u.dry_time)))

        # ugly hack for capturing target temp
        if u.dry_time > 0 and u.temp_target < int(u.temp_actual) - 1:
            u.temp_target = int(u.temp_actual)
        elif u.dry_time == 0:
            u.temp_target = 0

        if "info" in ams_u:
            u.ams_info = int(ams_u["info"])
            p_ams = parseAMSInfo(u.ams_info)

            u.heater_state = p_ams["heater_state"]
            u.raw_extruder_id = p_ams["extruder_id"]

            if new_caps.has_dual_extruder:
                u.assigned_to_extruder = ActiveTool(
                    p_ams.get("h2d_toolhead_index", 15)
                )
                updates["extruders"][
                    u.assigned_to_extruder.value
                ].assigned_to_ams_id = u.ams_id

            rb = ams_root.get("tray_exist_bits")
            if rb is not None:
                eb = int(rb, 16) if isinstance(rb, str) else int(rb)

                # Calculate the bit shift based on the unit ID
                # Standard AMS: 0, 1, 2, 3 -> shift 0, 4, 8, 12
                # AMS-HT: 128, 129, 130, 131 -> shift 16, 20, 24, 28
                if id >= 128:
                    shift = 16 + (4 * (id - 128))
                    # AMS-HT is a 1-slot unit, so we check only range(1)
                    u.tray_exists = [bool((eb >> shift) & (1 << j)) for j in range(1)]
                else:
                    shift = 4 * id
                    # Standard AMS is a 4-slot unit, so we check range(4)
                    u.tray_exists = [bool((eb >> shift) & (1 << j)) for j in range(4)]

        cur_ams[id] = u

    updates["ams_units"] = list(cur_ams.values())

    # ACTIVE / TARGET TRAYS AND TOOL TEMP
    # if multi-extruder return the active one
    a_ext = next(
        (e for e in updates["extruders"] if e.id == updates["active_tool"].value),
        None,
    )
    if a_ext:
        updates["active_ams_id"] = (
            a_ext.assigned_to_ams_id if a_ext.active_tray_id not in (254, 255) else -1
        )
        updates["active_tray_id"] = a_ext.active_tray_id
        updates["target_tray_id"] = a_ext.target_tray_id

        updates["active_tray_state"] = a_ext.tray_state

        updates["active_nozzle_temp"] = a_ext.temp
        updates["active_nozzle_temp_target"] = a_ext.temp_target
    else:
        # otherwise process a single extruder printer update
        updates["active_nozzle_temp"] = float(
            p.get("nozzle_temper", base.active_nozzle_temp)
        )
        updates["active_nozzle_temp_target"] = int(
            p.get("nozzle_target_temper", base.active_nozzle_temp_target)
        )
        updates["active_tray_id"] = int(ams_root.get("tray_now", base.active_tray_id))
        if updates["active_tray_id"] == 255:
            updates["active_tray_id"] = -1
            updates["active_tray_state"] = TrayState.UNLOADED
        elif updates["current_stage_id"] == 24:
            updates["active_tray_state"] = TrayState.LOADING
        elif updates["current_stage_id"] == 22:
            updates["active_tray_state"] = TrayState.UNLOADING
        else:
            updates["active_tray_state"] = TrayState.LOADED

    if "active_tray_id" in updates:
        updates["is_external_spool_active"] = updates["active_tray_id"] in [254, 255]
    else:
        updates["is_external_spool_active"] = False

    if "active_tray_state" in updates:
        updates["active_tray_state_name"] = updates["active_tray_state"].name

    # GLOBAL METADATA & FANS
    raw_exist = ams_root.get("ams_exist_bits", base.ams_exist_bits)
    updates["ams_exist_bits"] = (
        int(raw_exist, 16) if isinstance(raw_exist, str) else int(raw_exist)
    )
    updates["ams_connected_count"] = bin(updates["ams_exist_bits"]).count("1")
    updates["ams_status_raw"] = int(p.get("ams_status", base.ams_status_raw))
    updates["ams_status_text"] = parseAMSStatus(updates["ams_status_raw"])

    part_cooling_fan_speed_percent = -1

    if not config.capabilities.has_chamber_temp:
        part_cooling_fan_speed_percent = (
            scaleFanSpeed(p.get("cooling_fan_speed"))
            if p.get("cooling_fan_speed", -1) != -1
            else -1
        )
    else:
        part_cooling_fan_speed_percent = updates["climate"].zone_part_fan_percent

    if part_cooling_fan_speed_percent == -1:
        part_cooling_fan_speed_percent = base.climate.part_cooling_fan_speed_percent

    updates["climate"].part_cooling_fan_speed_percent = part_cooling_fan_speed_percent
    updates["climate"].part_cooling_fan_speed_target_percent = updates[
        "climate"
    ].part_cooling_fan_speed_percent

    heatbreak_fan_speed_percent = scaleFanSpeed(p.get("heatbreak_fan_speed", -1))
    if heatbreak_fan_speed_percent == -1:
        heatbreak_fan_speed_percent = base.climate.heatbreak_fan_speed_percent
    updates["climate"].heatbreak_fan_speed_percent = heatbreak_fan_speed_percent

    exhaust_fan_speed_percent = -1
    if not config.capabilities.has_chamber_temp:
        exhaust_fan_speed_percent = scaleFanSpeed(p.get("big_fan2_speed", -1))
    else:
        exhaust_fan_speed_percent = updates["climate"].zone_exhaust_percent

    if exhaust_fan_speed_percent == -1:
        exhaust_fan_speed_percent = base.climate.exhaust_fan_speed_percent
    updates["climate"].exhaust_fan_speed_percent = exhaust_fan_speed_percent

    aux_fan_speed_percent = -1
    if not config.capabilities.has_chamber_temp:
        aux_fan_speed_percent = scaleFanSpeed(p.get("big_fan1_speed", -1))
    else:
        aux_fan_speed_percent = updates["climate"].zone_aux_percent

    if aux_fan_speed_percent == -1:
        aux_fan_speed_percent = base.climate.aux_fan_speed_percent
    updates["climate"].aux_fan_speed_percent = aux_fan_speed_percent

    updates["wifi_signal_strength"] = p.get("wifi_signal", base.wifi_signal_strength)

    # ERROR HANDLING
    updates["print_error"] = int(p.get("print_error", base.print_error))

    if updates["print_error"] != 0:
        decoded_error = decodeError(updates["print_error"])
    else:
        decoded_error = {}
        base.hms_errors = []

    updates["hms_errors"] = decodeHMS(p.get("hms", base.hms_errors))
    if decoded_error and decoded_error not in updates["hms_errors"]:
        updates["hms_errors"].insert(0, decoded_error)

    # capabilities mapped to BambuConfig
    config.capabilities = new_caps

    return replace(base, **updates)

ExtruderState dataclass

Python
ExtruderState(
    id: int = 0,
    temp: float = 0.0,
    temp_target: int = 0,
    info_bits: int = 0,
    state: ExtruderInfoState = NO_NOZZLE,
    status: ExtruderStatus = IDLE,
    active_tray_id: int = -1,
    target_tray_id: int = -1,
    tray_state: TrayState = LOADED,
    assigned_to_ams_id: int = -1,
)

State for an individual physical extruder toolhead.

Attributes:

Name Type Description
active_tray_id int

The active tray for this extruder. Population: int(r.hnow)

assigned_to_ams_id int

The id of the ams associated with this extruder

id int

Physical ID. Population: int(r.get("id")).

info_bits int

Raw bitmask. Population: int(r.get("info")).

state ExtruderInfoState

Filament status. Population: parseExtruderInfo.

status ExtruderStatus

Op state. Population: parseExtruderStatus.

target_tray_id int

The target tray for this extruder. Population: int(r.htar >> 8).

temp float

Current Temp. Population: unpackTemperature(r.temp).

temp_target int

Target Temp. Population: unpackTemperature(r.temp).

tray_state TrayState

The tray state of this extruder

active_tray_id class-attribute instance-attribute

Python
active_tray_id: int = -1

The active tray for this extruder. Population: int(r.hnow)

assigned_to_ams_id class-attribute instance-attribute

Python
assigned_to_ams_id: int = -1

The id of the ams associated with this extruder

id class-attribute instance-attribute

Python
id: int = 0

Physical ID. Population: int(r.get("id")).

info_bits class-attribute instance-attribute

Python
info_bits: int = 0

Raw bitmask. Population: int(r.get("info")).

state class-attribute instance-attribute

Python
state: ExtruderInfoState = NO_NOZZLE

Filament status. Population: parseExtruderInfo.

status class-attribute instance-attribute

Python
status: ExtruderStatus = IDLE

Op state. Population: parseExtruderStatus.

target_tray_id class-attribute instance-attribute

Python
target_tray_id: int = -1

The target tray for this extruder. Population: int(r.htar >> 8).

temp class-attribute instance-attribute

Python
temp: float = 0.0

Current Temp. Population: unpackTemperature(r.temp).

temp_target class-attribute instance-attribute

Python
temp_target: int = 0

Target Temp. Population: unpackTemperature(r.temp).

tray_state class-attribute instance-attribute

Python
tray_state: TrayState = LOADED

The tray state of this extruder