aspic / breeze-core

Breeze Core

Self-hosted, LAN-first control for Midea air conditioners: a REST API, a web panel, device-pairing authentication, server-side schedules and temperature curves. No cloud account, no vendor app, nothing phoning home.

Add the aspic repository first — one snippet for your system, once — and then this is a single command. There is no separate repository for this project.
sudo apt install breeze-core        # Debian family
sudo dnf install breeze-core        # Fedora / RHEL family
sudo zypper install breeze-core     # openSUSE
sudo pacman -S breeze-core          # Arch family
sudo apk add breeze-core            # Alpine
sudo xbps-install -S breeze-core    # Void
sudo emerge -av app-misc/breeze-core-bin   # Gentoo (the -bin ebuild)
apk add breeze-core                 # OpenWrt 25.12+
opkg install breeze-core            # OpenWrt 24.10 and older
pkg install breeze-core             # Termux (Android)
pkgin -y install breeze-core        # NetBSD
pkg install breeze-core             # FreeBSD

OpenBSD and OPNsense install one named file rather than a package by name — the OpenBSD and OPNsense sections on the index have the command. Gentoo needs its overlay added rather than the package repository, since ebuilds are not packages.

Building it yourself

The source, through the same repository, built into the same package with your distribution's own Rust
dnf download --source breeze-core && rpmbuild --rebuild breeze-core-*.src.rpm   # Fedora / RHEL
apt source breeze-core && cd breeze-core-*/ && dpkg-buildpackage -b              # Debian / Ubuntu, with deb-src
# Arch: the newest tarball in /arch/sources/, then tar -xf breeze-core-*.src.tar.gz && cd breeze-core && makepkg -si

An SRPM, a Debian source package and a makepkg tarball (/arch/sources/, with a .sig beside each), each signed with the repository key and building offline from vendored crates. What comes out has the same name, files and breeze account as the package above, so either replaces the other; only the binary differs, linked against your glibc instead of static. Needs Rust 1.88 or newer, which Debian 13 does not have. The wiki has the build dependencies and every step.

After installing

Three steps, and the first one is the only one that needs your air conditioners switched on
# 1. find the units on your network and write the config
sudo breeze-core pair

# 2. bind somewhere useful: your LAN address for direct use, or leave it on
#    127.0.0.1 with a reverse proxy in front
sudoedit /etc/breeze-core/breeze-core.env

# 3. start it
sudo systemctl enable --now breeze-core

# and, if you want it reachable by name over HTTPS: nginx, Apache or
# Caddy, set up step by step, with --undo to take it all back out
sudo breeze-core proxy

Step 2's file moves with the platform's prefix: /usr/local/etc/breeze-core on FreeBSD and OPNsense, /usr/pkg/etc/breeze-core on NetBSD, %ProgramData%\breeze-core on Windows — or set AC_CONFIG_DIR once and stop thinking about it. Step 3 is whatever your init system is:

systemd

Debian · Ubuntu · Fedora · RHEL · SUSE · Arch · Gentoo (systemd profile)
sudo systemctl enable --now breeze-core
sudo systemctl status breeze-core
journalctl -u breeze-core -f            # follow the log

OpenRC

Alpine · Artix (openrc) · Gentoo · Devuan
sudo rc-update add breeze-core default
sudo rc-service breeze-core start
sudo rc-service breeze-core status

On Gentoo the settings live in /etc/conf.d/breeze-core, which is where OpenRC expects them — not in breeze-core.env.

runit

Void · Artix (runit)
# The Void package already ships /etc/sv/breeze-core, disabled.
# Enabling it is the symlink and nothing else:
sudo ln -s /etc/sv/breeze-core /var/service/
sudo sv status breeze-core

Installed by hand instead? The templates are deploy/init/runit-run and its companion runit-log-run, which gives you a rotated log under /var/log/breeze-core rather than runit's catch-all.

s6 / s6-rc

Artix (s6) · Obarun
sudo mkdir -p /etc/s6/sv/breeze-core
sudo install -m 0755 deploy/init/s6-run /etc/s6/sv/breeze-core/run
sudo s6-rc-bundle-update add default breeze-core
sudo s6-rc -u change breeze-core

SysV init

Devuan (sysvinit) · Slackware · older LTS releases
sudo install -m 0755 deploy/init/sysv-breeze-core /etc/init.d/breeze-core
sudo update-rc.d breeze-core defaults   # RHEL-ish: chkconfig --add
sudo service breeze-core start

procd

OpenWrt
/etc/init.d/breeze-core enable
/etc/init.d/breeze-core start
logread -e breeze-core                  # follow the log

termux-services

Termux (Android)
sv-enable breeze-core                   # now, and with every Termux session
sv status breeze-core
tail -f $PREFIX/var/log/sv/breeze-core/current

The package ships the service switched off, as Void's does. Android still stops a background Termux unless it holds a wake lock — termux-wake-lock — and the Termux:Boot add-on is what starts it again after a reboot.

rc.d — FreeBSD and OPNsense

FreeBSD 14 · 15
sudo sysrc breeze_core_enable=YES
sudo service breeze_core start
sudo service breeze_core status

Underscore, not hyphen: FreeBSD's rc system needs a name that is a valid shell variable. On OPNsense the GUI does this for you — Services > Breeze Core.

rc.d — NetBSD

echo 'breeze_core=YES' | sudo tee -a /etc/rc.conf
sudo service breeze_core start

rcctl — OpenBSD

doas rcctl enable breeze_core
doas rcctl start breeze_core
doas rcctl check breeze_core

OpenBSD has no service(8) at all; rcctl is the whole interface.

launchd

macOS — a LaunchDaemon, not an Agent
sudo cp deploy/init/com.breeze.core.plist /Library/LaunchDaemons/
sudo chown root:wheel /Library/LaunchDaemons/com.breeze.core.plist
sudo launchctl load /Library/LaunchDaemons/com.breeze.core.plist

A daemon rather than an agent, because an agent only runs while its user is logged in — the wrong lifetime for something a phone talks to. The plist creates no account; the header comments carry the three dscl lines.

supervisord

Anywhere, and the usual answer inside a container that runs more than one thing
sudo cp deploy/init/supervisor-breeze-core.conf \
  /etc/supervisor/conf.d/breeze-core.conf
sudo supervisorctl reread && sudo supervisorctl update

Windows service

Registered by the installer, supervised by NSSM
Get-Service BreezeCore
Restart-Service BreezeCore
Get-Content "$env:ProgramData\breeze-core\logs\service.log" -Tail 40 -Wait

Containers

docker compose up -d
docker logs -f breeze-core

Writing your own? Three things matter. Pass serve explicitly — a bare breeze-core prints its usage and exits 0, which as a start command looks like success and leaves you with no server. Run it in the foreground and let your supervisor own it; there is no --daemonize. And stop it with a plain TERM: every store is written to a temporary file and renamed into place, so no signal can catch one half-written.

Then open http://<that address>:8420. The panel asks for the API key once per device — it is in /etc/breeze-core/config.json, which pair wrote — and then asks an admin on the local network to approve the pairing:

sudo breeze-core approve --config /etc/breeze-core/config.json <CODE>
sudo breeze-core devices     # what is enrolled
sudo breeze-core diag        # is anything wrong
A V3 unit needs a token and key. Nearly every unit sold now is V3, and Midea will only issue those credentials to the account the unit is registered to — its cloud stopped handing them out to anyone else. pair takes a token and key if you already have them; if you don't, sudo breeze-core fetch asks for that account (MSmartHome, or Meiju in China) once, checks each token on the unit, and forgets the password. It also explains what to do if your units came with NetHome Plus, which no longer hands tokens out at all. Keep a copy of config.json somewhere else: those credentials may not be issued again.

From the command line

The whole server is one binary, and it is also the client
breeze-core control 'kitchen' cool 25.5 both auto turbo 15m
breeze-core control 'living room' heat 22 horizontal low eco
breeze-core control 'back room' none none none none none 30m

Name, mode, temperature, flap, fan, extra, timer — in that order, with none valid anywhere and names matched case insensitively (or by any unambiguous prefix). It enrols itself the first time you use it, like any other client.

On Windows

A guided installer, a service, and an optional reverse proxy — no interpreter to install first

Breeze-Core-Setup-4.3.2.exe (1.2 MB · sha256 · metalink) lays down one executable, registers a hardened service under NSSM as LOCAL SERVICE, locks down %ProgramData%\breeze-core, and opens one firewall rule restricted to your local subnet.

A first install needs the internet. Setup downloads two small helpers, each checked against a checksum pinned in the installer: NSSM, which runs the server as a service, and the updater. NSSM is fetched rather than carried because antivirus products flag installers that embed it. An upgrade reuses the NSSM already installed, so it needs no internet. On a machine that never has any, put nssm-2.24.zip next to the installer. Once installed, it checks aspic for a newer release when an administrator signs in, and asks before downloading anything; you can turn that off.

Public HTTPS is a separate, optional component — a guided Caddy wizard that fetches Caddy, writes a hardened config with automatic certificates and a LAN-only admin gate, and can install a fail2ban-style watcher that bans abusive addresses through Windows Firewall. Your LAN is never banned.

Setup asks Simple or Advanced. Simple serves your local network on port 8420; Advanced lets you choose the folder, the port, whether the network or only this PC can reach it, the firewall rule and the update check. Then: Pair AC units from the Start menu (or breeze-core pair), and Start-Service BreezeCore. To change the address or port later, run the installer again and choose to review the settings. Uninstalling always keeps %ProgramData%\breeze-core, with your units' tokens in it; remove that folder by hand for a full wipe.

On a Mac, or with Homebrew on Linux

One formula, prebuilt for Apple silicon, Intel Macs, and x86-64 and ARM64 Linux
brew tap aspic/breeze https://aspic.salataputarica.hr.eu.org/homebrew/breeze.git
brew trust aspic/breeze     # once: Homebrew asks this of every tap outside its own
brew install breeze-core

# 1. find the units on your network and write the config
breeze-core pair

# 2. bind somewhere useful: this machine's LAN address, for phones
"${EDITOR:-vi}" "$(brew --prefix)/etc/breeze-core/breeze-core.env"

# 3. start it, now and at every login
brew services start breeze-core

The settings file is the same breeze-core.env the Linux packages use, kept in $(brew --prefix)/etc/breeze-core with the rest of the store, and brew upgrade never overwrites it. brew services runs Breeze Core as you, while you are logged in: a launchd agent on macOS, a systemd user unit on Linux. On a Linux server, the distribution packages above are the better fit, because they run it as its own account from boot.

The Mac binaries need macOS 13 or newer. They are ad-hoc signed, not notarised, which Homebrew does not mind: it downloads with curl, so nothing is quarantined. Uninstalling keeps the store, as every package here does.

What you get

one binary~2.5 MB, no interpreter, no runtime, nothing installed alongside it. The web panel is compiled into it
the API30 endpoints, key + per-device-token auth, an SSE stream for live state, Prometheus metrics
schedulesfavourites, weekly schedules and interpolated temperature curves, run server-side — nothing has to stay open
an Android appBreeze, which talks to the same API
runs on22 targets. Linux on x86-64, 32-bit x86, arm64, armv7, armv6 (every Raspberry Pi), riscv64, POWER in both endiannesses, IBM Z, LoongArch, MIPS in both endiannesses and Cavium Octeon; OpenWrt routers and OPNsense firewalls; FreeBSD, NetBSD and OpenBSD; Windows; macOS on Apple silicon and Intel; Android phones under Termux. Each is run before it ships. And, untested and for fun, sixteen oddities: illumos, Solaris, DragonFly, Haiku, SPARC, the BSDs on ARM, Windows on ARM and a watch.
package managersapt · dnf · zypper · pacman · apk · xbps (Void, glibc and musl) · emerge (a Gentoo overlay) · opkg and OpenWrt's apk · pkg · pkgin · pkg_add · Termux's apt, plus a Windows installer, an OPNsense plugin and container images
init systemssystemd, OpenRC, runit, s6, SysV, procd, the three BSD rc flavours, launchd, supervisord, termux-services and a Windows service — templates for every one of them

Migrating from 3.x

One script: it works out your package manager, backs everything up, swaps bolero for aspic and upgrades the package
Do not trust scripts you find on the internet. Not this one either. What you are being offered is a file that runs as root on a machine you care about, from a server you have no particular reason to trust, and the fact that this paragraph exists proves nothing — it was written by the same people who wrote the script.

So read it. It is mostly comments, and the parts that delete things are marked. The checksum tells you the download was not corrupted; it cannot tell you the file is safe, because it comes from the same place. Reading is the only real check you have.
curl -fsSL https://aspic.salataputarica.hr.eu.org/migrate.sh -o migrate.sh
less migrate.sh            # the important step
sudo sh migrate.sh         # prints the plan, changes nothing
sudo sh migrate.sh --yes   # does it

Or, once you have read it: curl -fsSL https://aspic.salataputarica.hr.eu.org/migrate.sh | sudo sh -s -- --yes

Run without --yes it prints what it found and what it would change, and stops. That is the default on purpose.

it detectsyour OS, architecture and package manager — apt, dnf, zypper, pacman, apk, opkg, pkgin, pkg (FreeBSD) or pkg_add (OpenBSD)
it backs up/etc/breeze-core and your repository files and keys, into a timestamped root-only directory, with a generated ROLLBACK.sh holding the exact commands for your machine — not "restore from your backup"
it removesthe bolero repository and its signing key
it addsthe aspic repository and its key
it upgradesbreeze-core 3.x to 4.x. The package name is unchanged, so this is an upgrade in place: your configuration, device tokens, programs and timers are untouched and the service stays enabled
it leaves alonea service that was already stopped, and anything it cannot do safely — a source install is refused rather than half-converted

Verified by installing the real Python 3.2.0 from bolero and migrating it: Debian 12, AlmaLinux 9, Arch and Alpine 3.20 in clean containers, and NetBSD 11 and FreeBSD 15.1 on real machines — on FreeBSD that means 3.2.0 with its python312 dependency replaced by a 2.4 MB executable with none, config and device tokens untouched.

OpenBSD is the one case it refuses. The Python line only ever installed from source there, so pkg_add owns none of it and cannot replace it — and removing a service and a virtualenv is not something this script will do behind your back. It stops and prints the three commands to do it yourself, keeping your configuration; run it again afterwards and it installs the native package. Both halves of that were walked on OpenBSD 7.9.

Coming from 3.x

The Python implementation, still published on bolero

4.3.2 is the same project reimplemented — same REST API, same config.json, devices.json, programs.json and timers.json, same AC_* environment variables, same service name, same web panel and the same Android app. It is meant to be installed over a 3.x deployment rather than migrated to, and the existing paths it does not need any more are kept as symlinks so nothing that names them breaks.

Everything published for 3.x stays exactly where it is on bolero and keeps installing; nothing is being switched off underneath anyone.

Back up /etc/breeze-core before any upgrade. Not because this one is risky — an upgrade keeps the directory — but because a paired V3 unit's credentials cannot be obtained again, and that is true whatever you are upgrading.