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.
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.
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.
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.50Building 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:latestexists, but islinux/amd64only 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.
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 |
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 asOFF.heating(true/false/null) — the same signal, withnullkept 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.
heatinghas one blind spot. Once the chamber reaches target the element cycles to maintain, temperature goes flat, and this readsfalsedespite 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.
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.
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.
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.
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.
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"| 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 |
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 |
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
/statusand never logged. - Bind the bridge to
127.0.0.1or 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.
heatingis 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.
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.
MIT — see LICENSE.
Not affiliated with BIGTREETECH/BIQU. "Panda Breath" is their trademark; this is an independent, unofficial integration.