wiim2mqtt
v1.1.0
Published
Interface between WiiM (LinkPlay) audio streamers and MQTT
Maintainers
Readme
wiim2mqtt
Control WiiM audio streamers via MQTT
Connects to a WiiM (Pro, Pro Plus, Mini, Amp, Ultra — all LinkPlay based) over UPnP events and the LinkPlay HTTP API and bridges it to an MQTT broker, following the mqtt-smarthome architecture. Announces the device to Home Assistant via MQTT discovery.
State changes (play/pause, track, volume, …) arrive as UPnP events within milliseconds; the HTTP API is used for commands, for everything UPnP does not expose (presets, device info, multiroom) and as a polling fallback when events are not available.
Install
npm install -g wiim2mqttRequires Node.js ^20.19, ^22.12 or >= 24. wiim2mqtt 1.x is built on
mqtt-interfaces-core (mqtt-smarthome spec
2.x) like the author's other xyz2mqtt adapters; see "Upgrading from 0.1" below.
Usage
wiim2mqtt --address <wiim-ip> --mqtt-url mqtt://<broker>| option | default | description |
| --------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------ |
| -a, --address | | ip or hostname of the wiim, required |
| --api-url | | base url of the http api (default https://; e.g. http:// for other linkplay devices) |
| --upnp | on | subscribe to upnp events (use --no-upnp for polling only, no listener) |
| --upnp-port | 49152 | port of the upnp description/control/event interface on the device |
| --callback-host | | address the wiim uses to reach the upnp event listener (docker/wsl/multi-homed hosts; default: auto) |
| --callback-port | 0 | fixed port of the upnp event listener (0 = random free port; --install assigns one per instance) |
| --poll-interval | 1 | seconds between http status polls while playing when upnp events are unavailable |
| --position-interval | 1 | seconds between status/position updates while playing (0 = off) |
| --album-art-data | off | also publish the cover image bytes on /status/album_art_data |
| --raw-set | off | accept raw http api commands on /set/cmd (unrestricted remote api!) |
| -u, --mqtt-url | mqtt://localhost | mqtt broker url, e.g. mqtt://broker or mqtts://user:pass@broker:8883 |
| --mqtt-username | | mqtt broker username |
| --mqtt-password | | mqtt broker password |
| --mqtt-client-id-prefix | | prefix for the mqtt client id (_) |
| --mqtt-tls-ca | | path to a CA certificate file for mqtts:// connections |
| -n, --name | wiim | instance name. used as mqtt client id and as prefix for topics |
| --json-payloads | on | publish status as JSON {"val": ..., "ts": ..., "lc": ...} (use --no-json-payloads for plain values) |
| --ha-discovery | on | publish Home Assistant MQTT discovery (use --no-ha-discovery to disable and clear) |
| --ha-prefix | homeassistant | Home Assistant discovery prefix |
| --maintenance | on | accept /maintenance/set/loglevel and /restart over mqtt (use --no-maintenance to disable) |
| -v, --verbosity | info | log level (error, warn, info, debug) |
| --install / --uninstall | | install / remove the systemd service wiim2mqtt@<name> (needs root) |
| --config-schema | | print the JSON Schema of all options and exit |
Every option can also be set via environment variable with the prefix WIIM2MQTT_, e.g.
WIIM2MQTT_ADDRESS=192.168.1.30 WIIM2MQTT_MQTT_URL=mqtt://broker wiim2mqtt; the broker
settings fall back to the unprefixed MQTT_URL, MQTT_USERNAME, MQTT_PASSWORD,
MQTT_TLS_CA. Precedence: command line > environment > defaults. --config-schema prints a
JSON Schema of all options (with x-env and x-secret annotations) for management UIs.
UPnP events and the callback listener
The WiiM pushes state changes to an HTTP listener that wiim2mqtt opens (UPnP GENA). The device must be able to reach it:
- the listener address is detected automatically (the interface that routes to the WiiM);
override it with
--callback-hoston multi-homed hosts, WSL or Docker bridge networks. - the port is random unless
--callback-portis given.--installassigns a fixed port per instance (49200, 49201, …) so firewall rules stay stable. - If no event arrives within 30 s of subscribing, wiim2mqtt logs a warning naming the
callback URL, keeps working with HTTP polling (
status/upnp=polling) and retries the subscription in the background.--no-upnpturns the listener off altogether.
Run as a systemd service
sudo wiim2mqtt --install --name wiim --address 192.168.1.30 --mqtt-url mqtt://192.168.1.2--install creates a system user wiim2mqtt, writes the given options to
/etc/wiim2mqtt/<name>.env (WIIM2MQTT_* variables — edit and
systemctl restart wiim2mqtt@<name> to change), installs the template unit
/etc/systemd/system/[email protected] and enables + starts wiim2mqtt@<name>. The
instance name is the --name option, i.e. the MQTT topic prefix. Broker settings shared by
all mqtt-interfaces adapters on the host can go to /etc/mqtt-interfaces/broker.env
(MQTT_URL, MQTT_USERNAME, MQTT_PASSWORD). Logs: journalctl -u wiim2mqtt@<name> -f.
Several devices: run --install once per device with a different --name — each
becomes its own instance with its own config, topic prefix and callback port, sharing one
template unit and one system user:
sudo wiim2mqtt --install --name wiim-living --address 192.168.1.30 --mqtt-url mqtt://broker
sudo wiim2mqtt --install --name wiim-kitchen --address 192.168.1.31 --mqtt-url mqtt://broker
systemctl status 'wiim2mqtt@*'sudo wiim2mqtt --uninstall --name wiim-kitchen removes one instance (the template unit goes
with the last one). she can install, configure, update
and restart instances from its Services page; the config form comes from --config-schema.
Docker
docker run -d --name wiim2mqtt --network host --restart unless-stopped \
-e WIIM2MQTT_ADDRESS=192.168.1.30 \
-e WIIM2MQTT_MQTT_URL=mqtt://broker \
ghcr.io/hobbyquaker/wiim2mqtt--network host lets the WiiM reach the event listener. With a bridge network instead,
publish the port and tell the adapter the host's address:
-p 49200:49200 -e WIIM2MQTT_CALLBACK_PORT=49200 -e WIIM2MQTT_CALLBACK_HOST=<docker-host-ip>.
--restart unless-stopped brings the container back after maintenance/set/restart.
Deploy to a host (developers)
deploy.sh [user@host] (or npm run deploy) runs the tests, packs the module, copies it to
the host, installs it under /usr/local/lib/node_modules/wiim2mqtt and restarts every
wiim2mqtt@<name> service there (SERVICE, REMOTE_DIR, SKIP_TESTS env vars, see the
script). Install the service once with --install first.
Finding the device
wiim2mqtt --discoversends an SSDP M-SEARCH for UPnP media renderers and reads the description each answer points at, keeping the ones LinkPlay built:
172.16.23.189 Hobbyraum-WiiM WiiM Pro Plus Receiver [upnp] (ssdp)The name is the one you gave the device in the WiiM app. --discover-json prints the same as
JSON (with the UDN and the description URL). -a auto runs the scan at start and uses what it
found, refusing to start when none or more than one device answers rather than bridging the
wrong speaker:
wiim2mqtt -a auto -u mqtt://brokerA neighbour's TV or a Sonos answers the same M-SEARCH; only a description naming LinkPlay (or WiiM) is kept, so those are dropped rather than reported.
SSDP is link-local — it does not cross a router. For a WiiM on another VLAN, name it or the range it is in; port 49152 (the description) is checked over TCP, which routes fine:
wiim2mqtt --discover --discover-address 172.16.20.30
wiim2mqtt --discover --discover-address 172.16.20.0/24The scanning itself lives in mqtt-interfaces-core; this adapter only declares the search and what counts as a WiiM (lib/discovery.js).
Topics
<name> defaults to wiim.
<name>/connected
Retained. 0 = not connected to the broker (set via last will), 1 = connected to the
broker but the WiiM's HTTP API is unreachable, 2 = connected to both. Whether UPnP events
are flowing is reported separately in status/upnp.
<name>/info and <name>/maintenance/set/…
<name>/info (retained JSON) describes the running instance: package name and version,
mqtt-smarthome spec version, node version, host, pid, start time, the device address and api
url, and a device block with the WiiM's name, model, firmware, uuid, mac, ip and the current
state source (upnp: events, polling or off).
<name>/maintenance/set/loglevel (error|warn|info|debug) changes the log level at runtime,
<name>/maintenance/set/restart exits cleanly so the service manager restarts the process;
--no-maintenance turns both off.
<name>/status/<item>
Retained status reports, published on start and whenever the device reports a change (also
when changed via the WiiM app, AirPlay, …). Every status is
{"val": <value>, "ts": <ms received>, "lc": <ms last changed>}; with --no-json-payloads
the plain value (lists as JSON arrays).
| item | type | set | notes |
| ----------------------------------------------------------------------------------- | -------- | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| play_state | string | yes | playing, paused, stopped, loading |
| source | string | yes | wifi, airplay, dlna, spotify, tidal, bluetooth, line_in, optical, usb, hdmi, coaxial, phono, multiroom; only the ones in source_list can be set |
| source_list | string[] | | selectable sources |
| volume | int | yes | 0–100 |
| mute | bool | yes | |
| title, artist, album, album_art | string | | empty while stopped; album_art is the URL the device reports |
| album_art_data | bytes | | the cover image itself (raw bytes, not JSON), only with --album-art-data |
| quality, sample_rate, bit_depth, bitrate, origin_source | mixed | | e.g. LOSSLESS, 44100, 16, 1411, Tidal (depends on the source) |
| duration | int | | seconds |
| position | int | yes | seconds, not retained, published every --position-interval s while playing (interpolated locally) |
| repeat | string | yes | off, one, all |
| shuffle | bool | yes | |
| queue_index, queue_length | int | | position in / length of the device's play queue |
| preset_list, preset_max | JSON | | [{number, name, source, pic}] and the number of preset slots |
| group_role, group_master, group_slaves | mixed | | standalone / master / slave, the master's ip, [{name, ip, uuid}] |
| device_name, model, firmware, update_available, rssi, ip, mac, uuid | mixed | | device info, refreshed every 60 s |
| firmware_update | JSON | | {installed_version, latest_version} for the Home Assistant update entity |
| upnp | string | | events (UPnP events drive the state), polling (events unavailable, polling), off (--no-upnp) |
| cmd_result | string | | response of a raw cmd, not retained, only with --raw-set |
<name>/set/<item>
Change requests. Payload is a plain value or mqtt-smarthome style JSON ({"val": 12}).
Booleans accept true/false, 1/0, on/off.
| item | payload | notes |
| ----------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------ |
| volume | 0–100 | |
| volume_up, volume_down | optional step (default 5) | |
| mute | bool | |
| play | empty, or a stream URL | resume, or play the given URL |
| pause, stop, next, prev, toggle | any | |
| play_state | playing, paused, stopped | |
| seek / position | seconds or mm:ss | |
| source | name from source_list | |
| repeat | off, one, all | |
| shuffle | bool | |
| preset | number or name | 1..preset_max; names from preset_list (case-insensitive) |
| join | ip of the master | this device joins the master's group |
| leave | any | this device leaves its group (sent to the master) |
| ungroup | any | dissolve the group this device masters |
| kick | ip or name of a slave | |
| reboot | true | |
| refresh | status (default), presets, device, group, metadata | explicit re-read of the device state |
| cmd | raw http api command | only with --raw-set; the response goes to status/cmd_result (not retained) |
mosquitto_pub -t wiim/set/volume -m 12
mosquitto_pub -t wiim/set/preset -m "Radio One"
mosquitto_pub -t wiim/set/source -m bluetooth
mosquitto_pub -t wiim/set/repeat -m all
mosquitto_pub -t wiim/set/refresh -m presetsEvery set is verified by re-reading the affected items after 500 ms; a set that changed nothing is logged as a warning.
Home Assistant
MQTT discovery is on by default (HA ≥ 2024.11, device-based discovery). The WiiM appears
as one device with: volume (number), mute and shuffle (switches), source / repeat / preset
(selects), play/pause/stop/next/previous buttons, sensors for play state, title, artist,
album, quality, sample rate, bit depth, bitrate, an album art image entity, a firmware
update entity and diagnostic sensors. Availability follows <name>/connected.
--no-ha-discovery disables discovery and removes the device announcement on startup;
--ha-prefix changes the discovery prefix. HA has no MQTT media player platform; a payload
for a community media player component is planned.
Upgrading from 0.1
wiim2mqtt 1.0 moves to mqtt-interfaces-core. Status and set items are unchanged; what changed:
| 0.1 | 1.0 |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| plain status payloads, --json-payloads opt-in | {val, ts, lc} JSON by default; --no-json-payloads for plain values |
| <name>/get/<what> for an explicit refresh | <name>/set/refresh with the same payload (status, presets, device, group, metadata) |
| <name>/info with uptime, re-published every minute | <name>/info per the core convention (spec, started, maintenance, …), re-published when the device facts change |
| --probe | removed; use node scripts/dump.js <address> from a checkout |
| --api-url alone | --address is always required, --api-url only overrides the url |
| — | <name>/maintenance/set/{loglevel,restart}, --config-schema, --mqtt-client-id-prefix, --mqtt-tls-ca, MQTT_* fallback variables |
| /etc/wiim2mqtt/<name>.env + own template unit | same file (the callback port is kept); the template unit is rewritten by --install (shared broker.env, state directory /var/lib/wiim2mqtt/<name>, Restart=always) — run sudo wiim2mqtt --install --name <n> … once per instance |
Home Assistant: the device id (wiim2mqtt_<name>) and entity unique ids are unchanged, so the
device and its history are kept; the discovery payload is re-published on start.
Verification status
Developed against a WiiM Pro Plus (firmware 4.8.814734). Other models share the platform but may differ in
available sources (hdmi, phono, coaxial on Amp/Ultra) and in details of the API.
node scripts/dump.js <address> --listen 20 prints a read-only dump of your device (api
responses, UPnP description, 20 s of events) — attach it to issues about missing or wrong
items.
License
MIT © Sebastian Raff
