Skip to content

Repository files navigation

panda-breath-bridge

A tiny HTTP shim that puts the BIQU Panda Breath chamber heater on a REST API, so it can be driven by tools that speak HTTP — Bambuddy, Home Assistant, Node-RED, a cron job, curl.

The Panda Breath only speaks an undocumented WebSocket protocol. This bridge translates.

GET  /status?host=<ip>   →  {"state":"ON","heating":true,"chamber_temp":47, ...}
POST /on?host=<ip>       →  power the unit up  (optionally &set_temp=40-60)
POST /off?host=<ip>      →  power the unit down

Roughly 250 lines of Node, no dependencies beyond ws, one small container.


Why this exists

The Panda Breath is controlled entirely from its own web page. It reports a chamber temperature nothing else can read, and there is no way to switch it, script it, or graph it from the tools already running your printer.

This bridge exposes it over HTTP so your print server or home automation can. What you get:

  • Live chamber telemetry — a second, independent chamber reading alongside your printer's own, plus a derived "is it actually warming the chamber right now" signal.
  • Remote power — switch the unit on and off without opening its web UI.
  • The chamber target — set the temperature it heats to, from wherever you configure everything else.

It stays in Auto Mode, deliberately

The device has three work modes. Two matter:

Mode Behaviour
Auto Mode Conditional. In the device's own words, "chamber heater will turn on when Heatedbed temperature is set to above this threshold" — the Heater Activation Threshold Temperature, 80 °C by default.
Power On Manual. Heats on command, whatever the bed is doing.

The bridge never leaves Auto Mode, and that is the safety-critical decision in the whole project.

Auto Mode is filament-aware for free, because bed temperature tracks filament: an ASA plate at 90 °C clears an 80 °C threshold, a PLA plate at 60 °C does not. The device decides correctly, per print, with no configuration and nothing to remember.

Power On knows nothing about what is loaded. Driving it from an automation would force chamber heat onto every print including PLA — heat creep, jams, softened parts. So /on powers the unit up and lets Auto Mode decide; it does not force heat. That keeps even "turn on when print starts" safe.

Quick start

Clone the repo, then:

# docker-compose.yml
services:
  panda-bridge:
    build: .
    container_name: panda-bridge
    restart: unless-stopped
    ports:
      - "127.0.0.1:8977:8977"
    environment:
      # Optional. Requests may pass ?host=<ip> instead, which is usually
      # nicer: the address then lives in your print server's settings and
      # changing it needs no restart.
      - PANDA_HOST=192.168.1.50

Building takes a few seconds — it is a single small file on top of node:22-alpine.

Prefer a pre-built image? ghcr.io/ticfinack/panda-breath-bridge:latest exists, but is linux/amd64 only at the moment, so it will not run on a Raspberry Pi or Apple Silicon. Multi-arch publishing is pending. Until then, build: . above is the path that works everywhere.

$ docker compose up -d
$ curl -s "http://localhost:8977/status?host=192.168.1.50" | jq
{
  "host": "192.168.1.50",
  "connected": true,
  "state": "OFF",
  "enabled": true,
  "heating": false,
  "work_mode_name": "auto",
  "chamber_temp": 34,
  "chamber_trend": { "rise_c": 0, "span_seconds": 78, "c_per_min": 0 },
  "at_target": false,
  "settings": { "set_temp": 60, "hotbedtemp": 80, ... }
}

Bind to 127.0.0.1 unless you need it reachable from another host — see Security.

API

Every endpoint takes an optional ?host=<ip-or-hostname>, falling back to PANDA_HOST. One bridge serves any number of devices.

Endpoint Does
GET /health Liveness, plus which device sessions are open
GET /status Full state (below)
POST /on Power the unit up. Optional &set_temp=<40-60> sets the chamber target too
POST /off Power the unit down

The two different questions /status answers

These get confused, so they are separate fields:

  • state ("ON"/"OFF") — is the chamber actually being heated right now? This is the field to point a switch integration at, so the indicator means "heat is going in" rather than "the unit is plugged in". Unknown reports as OFF.
  • heating (true/false/null) — the same signal, with null kept for "not enough recent samples to say". Use this if you want to distinguish unknown from no.

enabled carries the raw master switch, if you want to know the unit is powered even when idle.

Both are derived from the chamber temperature trend, because the device reports no element state at all — see Protocol notes.

heating has one blind spot. Once the chamber reaches target the element cycles to maintain, temperature goes flat, and this reads false despite the heater working. It answers "is it warming up", which is the useful question while you wait to print. The device gives us nothing better — see below.

Setting the chamber target

POST /on takes an optional set_temp, applied as the heater switches on:

$ curl -X POST "http://localhost:8977/on?host=192.168.1.50&set_temp=50"
{"ok":true,"host":"192.168.1.50","work_on":true,"mode":"auto","set_temp":50}

Valid range is 40–60 °C, matching the limits the device's own UI enforces; anything else is rejected with a 400 rather than silently clamped by the firmware.

This is the target Auto Mode heats to when it runs — it does not itself cause heating. Carrying it on the request rather than in a separate endpoint is deliberate: a print server whose plug integration only calls one fixed URL can still control the setpoint, by putting it in that URL.

Set it in the device's own frame of reference. Your printer's chamber sensor and the Panda's sit in different places and read differently under load, so this value is not the same number as your slicer's or print server's chamber target. Set it to what the filament wants and let the printer-side target stay whatever suits your printer's sensor.

Using it with Bambuddy

Bambuddy has a generic REST smart-plug type, so no Bambuddy changes are needed. The plug gives you live chamber state on the printer card, a switch for the unit's power, and — if you want it — automatic power-on when a print starts.

Because the bridge never leaves Auto Mode, that automation stays safe: switching the plug on powers the unit, and the device still decides for itself whether the bed is hot enough to warrant chamber heat. A PLA print does not get a heated chamber just because something flipped a switch.

Creating a plug is a two-step process: fill in the form, then set the automation toggles — those only appear once the plug exists.

Step 1 — create the plug

Settings → Smart Plugs → Add Smart Plug, then pick the REST tab. Field names below are exactly as Bambuddy shows them.

Replace 192.168.1.50 with your Panda Breath's IP in all three URLs.

Field Value
HTTP Method POST
Turn ON URL http://panda-bridge:8977/on?host=192.168.1.50 — append &set_temp=50 to also set the chamber target
ON Request Body leave empty
Turn OFF URL http://panda-bridge:8977/off?host=192.168.1.50
OFF Request Body leave empty
Custom Headers (JSON) leave empty
Status URL http://panda-bridge:8977/status?host=192.168.1.50
State JSON Path state
ON Value ON
Energy Monitoring (all fields) leave empty — the bridge reports no power data
Name Chamber Heat — short enough to survive Bambuddy's name truncation
Link to Printer the printer whose chamber this heats

Linking to a printer is what allows the automation in step 2.

Use http://panda-bridge:8977 when Bambuddy and the bridge share a Docker network — that is the container name. If Bambuddy runs elsewhere, use the bridge host's address (e.g. http://192.168.1.10:8977) and note the bridge binds to loopback by default, so you would need to publish it on the LAN interface first.

Because the device address is a URL parameter, changing your Panda's IP later is an edit to these three fields — no compose file, no container restart. The same applies to set_temp: putting it in the Turn ON URL means every automatic switch-on applies that chamber target, configured from Bambuddy rather than the device's web UI.

Bambuddy sends the URL exactly as typed, with no per-job substitution, so the setpoint is fixed per plug. Set it to what your chamber-heated filaments want; Auto Mode is what decides whether any given print gets heat.

Step 2 — set the automation toggles

Open the plug you just created; the toggles are under Automation Settings.

Toggle Why
Enabled ✅ on "Enable automation for this plug" — without it the rest do nothing.
Auto On ✅ on "Turn on when print starts" — powers the unit for the print. Safe: Auto Mode still decides whether to heat.
Auto Off off "Turn off when print completes" — cuts the unit exactly when you want the chamber held warm while you clear the plate.
Powers the printer off Bambuddy's own hint says to turn this off when the plug only powers an accessory. Left on, switching the plug off marks your printer offline.
Auto Off After Drying ❌ off Unrelated to chamber heat.
Show in Switchbar optional Puts it in the sidebar for one-click access.

Leave Auto Off disabled. It looks like the tidy counterpart to Auto On, but cutting power when a print completes is the opposite of what you want between back-to-back chamber-heated prints.

The plug card then shows whether the chamber is actually being heated, rather than whether the unit is plugged in — so an idle heater on a cold bed reads OFF, which is the honest answer.

Keeping the chamber warm between prints. Bambuddy's keep bed warm between prints setting holds the bed hot after a print finishes. If that held temperature is above the Panda's activation threshold — a 90 °C bed against an 80 °C threshold, say — Auto Mode keeps heating the chamber right through the plate-clearing window, with no plug automation involved at all.

Using it with Home Assistant

switch:
  - platform: rest
    name: Panda Breath
    resource: http://panda-bridge:8977/status?host=192.168.1.50
    state_resource: http://panda-bridge:8977/status?host=192.168.1.50
    is_on_template: "{{ value_json.state == 'ON' }}"
    body_on: ""
    body_off: ""
    turn_on:  { method: post, url: "http://panda-bridge:8977/on?host=192.168.1.50" }
    turn_off: { method: post, url: "http://panda-bridge:8977/off?host=192.168.1.50" }

sensor:
  - platform: rest
    name: Panda Breath chamber
    resource: http://panda-bridge:8977/status?host=192.168.1.50
    value_template: "{{ value_json.chamber_temp }}"
    unit_of_measurement: "°C"

Configuration

Variable Default Purpose
PORT 8977 Listen port
PANDA_HOST (none) Default device when a request omits ?host=
IDLE_TIMEOUT_MS 90000 Drop a device's socket after this long unused
CONFIG_MAX_AGE_MS 20000 How stale on/off state may get before re-reading

Protocol notes

There is no public specification for this device. Everything below was established by observation against firmware V1.0.4 and drove design decisions in the bridge — it is written down so the next person does not have to rediscover it.

Transport. Unauthenticated WebSocket at ws://<device>/ws. Commands are {"settings": {"<key>": <value>}}. The device accepts multiple concurrent clients — the bridge does not lock you out of the web UI.

Two frame shapes. On connect you get one full config frame. After that it streams {"settings":{"warehouse_temper":NN}} roughly once a second, and little else.

State fields only appear at connect. work_on and work_mode are in the config frame and are never re-sent — not after you change them, and not when the device changes them itself. A long-lived connection therefore cannot learn the current on/off state.

Nothing is broadcast. A client watching while a second client toggled the heater saw 25 frames and not one mentioned work_on. Combined with the point above, the only way to observe state is to open a fresh connection. That is why the bridge reconnects every CONFIG_MAX_AGE_MS, and why it optimistically applies its own commands.

There is no "element energised" field. The complete vocabulary, enumerated across 42 frames:

custom_temp, custom_timer, filament_button, filtertemp, fw_version, hotbedtemp,
isrunning, language, printer_type, remaining_seconds, set_temp,
warehouse_temper, work_mode, work_on

isrunning is not it — per the device's own JavaScript it tracks the filament-drying countdown. The device's web UI has no heating indicator either. Hence the measured heating field.

Field meanings

Field Meaning
work_on Master enable, 0/1
work_mode 1 Auto Mode (bed-threshold driven), 2 Power On (manual heat), 3 Filament Drying
set_temp Chamber target °C
warehouse_temper Chamber temperature °C, whole degrees
hotbedtemp "Heater Activation Threshold Temperature" — in Auto Mode the chamber heater runs only when the bed is set above this (default 80 °C)
filtertemp Filtration threshold °C
isrunning, remaining_seconds, custom_* Filament-drying cycle

Security

The device's opening frame contains your Wi-Fi password in plaintext, along with the paired printer's serial and LAN access code. That is the device's design, not this bridge's — any WebSocket client sees it, and the endpoint is unauthenticated.

Accordingly:

  • The bridge allowlists what it re-exposes. Credentials are never returned by /status and never logged.
  • Bind the bridge to 127.0.0.1 or a private Docker network. Do not publish it.
  • Keep the Panda Breath itself off any internet-reachable network segment.

The ?host parameter is validated against a strict hostname/IPv4 pattern before being interpolated into a URL.

Limitations

  • heating is inferred from the chamber trend, so it cannot see the element cycling once the chamber has reached target — it answers "is it warming up", not "is the element energised". The device exposes nothing better.
  • Power (master switch) and chamber setpoint are the only controls exposed. Work mode is deliberately left in Auto — see above — and filtration and drying are left to the device UI.
  • State is re-read on a timer, so a change made elsewhere surfaces within ~20 s rather than instantly. This is a device constraint, not a design choice.
  • Tested against firmware V1.0.4 only.

Contributing

Issues and PRs welcome — particularly confirmations or corrections against other firmware versions. If you find a field this document gets wrong, please say so; it was all derived by observation.

License

MIT — see LICENSE.

Not affiliated with BIGTREETECH/BIQU. "Panda Breath" is their trademark; this is an independent, unofficial integration.

About

HTTP/REST bridge for the BIQU Panda Breath chamber heater's undocumented WebSocket API — drive it from Bambuddy, Home Assistant, or anything that speaks HTTP

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages