-
Notifications
You must be signed in to change notification settings - Fork 0
Troubleshooting
Ordered by how often each one is the answer.
breeze-core --version # which build is actually running
systemctl status breeze-core # or rc-service / sv / rcctl
breeze-core diag # 30-odd checks against a running serverdiag is the fastest way to a diagnosis. It checks connectivity, that a
missing key is refused and a correct one accepted, that the API and
config.json agree about how many units exist, that every unit's state is
valid, that enum values come back as names rather than integers, and that the
diagnostic endpoints leak no secrets.
One thing
diagdoes that is worth knowing about. Its last check POSTstarget_temperature: 99to your first unit and expects a422— it is proving that a bad value is refused before it reaches the firmware. Under normal operation nothing is sent to the unit. But if that bounds check were ever broken, this is the check that would find out by sending 99 °C to a real air conditioner. If you are being careful with a live system, that is the one line to know about.
Check identity, not liveness. If something else still holds port 8420, the
new server cannot bind — and the old one goes on answering /api/health, so a
failed start looks like a success:
breeze-core --version # not curl /api/health
sudo ss -lntp | grep 8420 # who actually has the portThe server refuses to start without one, deliberately: a server with no key and
no units can only produce confusing 401s. Run sudo breeze-core pair.
Unless you are upgrading from an old install. Releases before 2.5.0
defaulted to /etc/meow-ac/config.json, and 4.x looks in /etc/breeze-core.
The startup message says so if it spots one. Do not run pair in that
situation — pairing writes a fresh config.json and cannot recover a paired
V3 unit's token and key, which are not re-issuable. Copy the old file across,
or point AC_CONFIG_DIR at the old directory.
BREEZE_HOST names an address this machine does not have — a common result of
copying an env file between hosts, or of a DHCP lease changing. Either set it to
an address ip -br addr actually lists, or use 127.0.0.1 and put a reverse
proxy in front.
503 means the request was valid and the unit did not cooperate. It is the
one status worth handling deliberately: retrying may work, sending something
different will not.
In order of likelihood:
- The unit is on a different subnet or VLAN from the server. Control is direct TCP to port 6444 on the unit; nothing routes it for you.
-
The unit's IP changed. DHCP moved it. Give the unit a static lease, or
find it again:
breeze-core pairsweeps the LAN and reports what answers, andGET /api/units/scandoes the same over HTTP. Then correct the address withPATCH /api/units/{id}, or in the panel. - Something else is holding the unit's single session. Midea firmware accepts one control connection at a time, so the vendor app in the foreground on a phone can lock out the server, and vice versa. Close it.
- The V3 credentials are stale. Rarer, and it looks like a unit that authenticates and then refuses everything.
GET /api/units/{id}/state on an unreachable unit returns its last known
values with online: false rather than an error, because clients render it
straight into a card. So a card that looks stale and says offline is this.
A wrong code, an expired code and an already-used code all return the same
404. That is deliberate: telling them apart would tell a guesser which of the
three they had achieved.
- The code lives 60 seconds by default (
AC_CODE_TTL). Type it promptly. - It is single-use. A second attempt with the same code fails even if the first succeeded.
- Approval must come from a private source address. The failure is a
403, not a401— the credentials were fine, the location was not.
Without --behind-proxy (or AC_BEHIND_PROXY=1) the server sees the proxy's
own address, which is itself private — so every proxied request passes the
LAN-only check and it stops meaning anything at all. With it set, the proxy
must actually forward the real client address.
The test. GET /api/system reports how this very request reached the
server, so it answers the question directly rather than by inference. Read
client_ip and behind_proxy_enabled as a pair: a client_ip of 127.0.0.1
while you are connecting from a phone means the real address is not arriving,
and a populated forwarded_for with behind_proxy_enabled: false means the
proxy is sending an address nobody is reading. Either way the LAN-only check is
deciding on the wrong address.
curl -s -H "X-API-Key: $KEY" -H "Authorization: Bearer $TOKEN" \
http://server:8420/api/system \
| grep -oE '"(client_ip|client_is_private|forwarded_for|behind_proxy_enabled)":[^,}]*'A rejected signature deliberately tells you nothing — except for clock skew, which is the one genuinely self-healing case:
{ "detail": { "error": "clock_skew", "retryable": true,
"server_time": 1788889209.7, "max_skew_seconds": 60 } }The server includes its own time so a client can compute its offset and retry.
If a device keeps failing this way, fix its clock (or the server's) rather than
widening AC_AUTH_SKEW_SECONDS — a wider window is a wider replay window.
426 Upgrade Required means the credential's auth version is below
AC_MIN_AUTH_VERSION. See Signed auth (v2) migration.
For a stubborn case there is a diagnostic that dumps what the server thought it was verifying — the canonical string, byte for byte:
sudo systemctl stop breeze-core
sudo -u breeze BREEZE_DEBUG_AUTH=1 /usr/bin/breeze-core serve \
--host 127.0.0.1 --port 8420Compare it with what your client signed. A mismatch is nearly always the path (the query string is part of it) or the body digest (SHA3-512, not SHA-2).
Everything scheduled uses the server's local clock. Not the phone's, not UTC.
In a container that means UTC unless you set TZ, and a schedule an hour
or two off is almost always this. Check what the server thinks:
curl -s -H "X-API-Key: $KEY" -H "Authorization: Bearer $TOKEN" \
http://server:8420/api/system | grep -o '"utc_offset_seconds":[0-9-]*'0 on a machine that should not be UTC means the timezone database is not
reachable. The container images ship the whole of /usr/share/zoneinfo for
this reason; a hand-built image without it reports 0 and fires everything
wrong, silently.
The poller idles until somebody subscribes, so with no subscribers the
server makes no LAN traffic at all and the first event after connecting can take
up to one tick (AC_STREAM_TICK, 5 s by default).
If events arrive in bursts, or stop while the connection stays open, a proxy is
buffering them. Never compress or buffer this stream — give nginx
proxy_buffering off or Caddy flush_interval -1.
If you have many units, one poll pass can outlast the tick: at roughly a second
per unit, more than about five means each tick starts later than the last. Raise
BREEZE_BG_WORKERS — see Configuration.
On 4.0.2, a burst of taps on + or − queued one command per tap on the unit, answered each one long after the tap, and held up every other unit meanwhile — so the number walked back through values already tapped past. 4.1.0 merges controls that arrive while a unit is busy and answers them together; upgrade the server, and the Breeze app to 2.2.8, which also stops showing a reply that a newer tap has overtaken.
If one unit is still slower than the others, open the Nerd panel and look at
that unit's link:
-
resendsclimbing means the unit keeps ignoring requests and being asked again. Every unit does this occasionally; one that does it often usually has a weak WiFi signal. -
connectsclimbing faster than you open the app means its connection keeps being dropped. With keep-warm on (the default) it should stay at one per unit for as long as you are using it.
Some changes are refused by the air conditioner itself. It still answers the command, with its state unchanged, so the control looks as if it sprang back. From 4.1.1 the web panel and the Breeze app (2.2.9) say so outright — "The air conditioner didn't accept the flap change — the unit refused it, not Breeze Core" — with the likely reason.
Measured on real units, one flap change each, straight after switching into the mode:
| Do the flaps change? | |
|---|---|
| switched off, any mode | no |
| fan, dry, cool, auto | yes, at once |
| heating, just switched on or into heating | no — held still |
| heating, once warm air is coming out (a few minutes) | yes |
- Flaps while heating. When a unit starts heating it holds its louvres still until the coil is warm, so it does not blow cold air at anyone, and it ignores flap changes until then — in cooling there is no such wait. This is firmware: Breeze Core 4.0.2 and msmart, the library the Python version used, were refused identically. Wait until warm air is coming out, then try again.
- Eco, and sometimes turbo, only in some modes. Many units offer eco only while cooling.
- Nothing moves while switched off. Mode and temperature are still taken and stored for next time; flaps are not.
A change that is refused where it should work — flaps in cooling, say — is
worth a report with the unit's model; the reply's not_applied field (see
REST API) says exactly which fields the unit turned down.
The service could not write its state directory. It runs as breeze and needs
to own /etc/breeze-core:
sudo ls -la /etc/breeze-core
sudo chown -R breeze:breeze /etc/breeze-core
sudo chmod 750 /etc/breeze-coreconfig.json is 0640, the other three are 0600, and the modes are enforced
on every write — so a file restored from a backup with loose permissions gets
tightened the next time the server saves it.
An access log is on by default: one line per request with its status and
how long it took. BREEZE_LOG=0 silences it.
BREEZE_DEBUG=1 adds a full trace of every control command — what the client
sent, what was applied, and what the unit echoed back:
sudo systemctl stop breeze-core
sudo -u breeze BREEZE_DEBUG=1 /usr/bin/breeze-core serve --host 127.0.0.1 --port 8420Run it in the foreground like that rather than editing the unit file: you get the output where you can read it, and stopping is Ctrl-C. Nothing is written to the config directory that a normal start would not write.
For the whole picture in one response — OS, init system, CPU, the four store
paths and their modes, the units and their cached capabilities, enrolled
credentials, scheduler and stream state, and how this request reached the
server — use GET /api/system. It never contains a secret, and diag checks
that omission rather than trusting it.
Include, at minimum:
breeze-core --version- the output of
breeze-core diag -
GET /api/system(safe to paste — no secrets in it) - the relevant lines from the access log
Never paste config.json. It holds the API key in plaintext and your
units' V3 credentials.
Breeze Core · Breeze for Android · Packages · AGPL-3.0
Start here
Install it
- From packages (apt/dnf/…)
- With containers
- On Windows
- On the BSDs
- On OPNsense
- From source (any OS/init/libc)
Use it
Reference
Run it safely
Coming from an earlier version
Develop and port