Go daemon for Linux x86 that speaks the Fermax Wi-Box protocol from the cloud generation (model IDS7938, Quvii/ubell SDK) and exposes a simple HTTP API for Home Assistant or other apps.
It replaces the phone as the gateway: you do not need the app open or the phone at hand.
- Login against the manufacturer cloud (
r1-8.qvcloud.net) over the userauth duplex channel. - Device list of the account.
- Device token (64-character auth-code and 32-byte encryption key).
- Door opening over QUII on TCP against the doorbell on the local network.
- Door opening remotely, in pure Go:
query-hlrv2discovery, UST MQTT broker,p2pconnect, UDP NAT hole punching probe, RB-UDP tunnel and QUII inside the tunnel. No manufacturer SDK (which only ships for arm64) and no phone involved. - Remote H.264 Annex-B video on stdout, so go2rtc can hand it to Home
Assistant. Optional G.711A audio, either on stderr or muxed together with the
video into a single MPEG-TS stream (
WIBOX_MPEGTS=1). - Doorbell discovery on the local network (
-discover): scans the QUII port in the machine's /24s and runs the setup against every candidate. - Token-protected HTTP API:
/health,/devices,/open.
- It does not serve RTSP and does not transcode: video comes out as raw H.264 Annex-B and go2rtc does the packaging.
- It is not an NVR and does not keep the door open: every
-videois one session. - The doorbell runs on batteries and sleeps. It wakes on physical activity or
when the official app is used, and the window is about 30 seconds. While it
sleeps,
p2pconnectreturnsaofflineand the daemon retries untilWIBOX_REMOTE_ATTEMPTSis exhausted.
go build -o wibox-daemon .
Tested with Go 1.27 on linux/amd64. It also builds for arm64:
GOARCH=arm64 go build -o wibox-daemon-arm64 .
There is a Makefile with the same commands, so the version and the binaries
cannot drift apart: make check runs gofmt, vet and the tests, make race adds
the race detector, make cover prints the coverage, make build writes the
local binary and make release writes both architectures into dist/ with the
version read from wibox/config.yaml.
Everything through environment variables (or a systemd EnvironmentFile):
| Variable | Purpose |
|---|---|
WIBOX_ACCOUNT |
Doorbell account (the one returned when accepting the invitation) |
WIBOX_PASSWORD |
Password of that account |
WIBOX_CLIENT_ID |
App client identifier (defaults to 003-2021-87dad215f30f1f7e) |
WIBOX_DEVICE_ID |
Doorbell UDID (when unset, the one passed in /open is used) |
WIBOX_UNLOCK_PASSWORD |
Opening PIN (no default, set it in the config file) |
WIBOX_LAN_ADDR |
ip:port of the doorbell on the local network (stream port, 34567 by default) |
WIBOX_LAN_PORT |
Port scanned by -discover (defaults to 34567) |
WIBOX_API_TOKEN |
HTTP API token. Empty = no authentication |
WIBOX_API_PORT |
API port (defaults to 8087) |
WIBOX_CHANNEL / WIBOX_STREAM |
QUII channel and stream (defaults to 1 and 2) |
WIBOX_BASE_URL |
Manufacturer cloud |
WIBOX_REMOTE |
0 disables the remote path (enabled by default) |
WIBOX_REMOTE_ONLY |
1 skips the local path even when WIBOX_LAN_ADDR is set |
WIBOX_REMOTE_ATTEMPTS |
p2pconnect attempts (defaults to 20, 12 s each) |
WIBOX_REMOTE_BUDGET |
Seconds the daemon keeps looking for the doorbell before giving up (default 240) |
WIBOX_AWAKE_POLL |
Seconds between probes while the doorbell sleeps (defaults to 3) |
WIBOX_VIDEO_WAIT |
Seconds without frames before cutting the video (defaults to 20) |
WIBOX_MPEGTS |
1 muxes video and G.711A audio into MPEG-TS, so one exec: pipe carries both |
WIBOX_AUDIO_FILE |
Writes the raw G.711A audio to that file instead of stderr |
WIBOX_PLAY_RESENDS |
Times the play is resent while waiting for a keyframe (defaults to 3, 0 disables it) |
WIBOX_VIDEO_ALL_FRAMES |
1 forwards video frames before the first keyframe (default off). Colours are wrong until a keyframe arrives, but there is a picture and the stream is well formed |
WIBOX_CONTROL_SOCK |
Unix socket where the video process takes door-opening requests (default /tmp/wibox-video.sock) |
WIBOX_LOG_FILE |
Also write the log to this file. Needed inside the add-on: go2rtc swallows its child stderr |
WIBOX_CLOUD_CHECK |
Seconds between cloud login checks reported in /status (default 600, 0 disables it) |
WIBOX_VIDEO_MAX |
Hard cap on one video session, in seconds (default 900, 0 disables it) |
WIBOX_VIDEO_LOCK |
Lock file that keeps a second video process off the doorbell (default /tmp/wibox-video.lock) |
WIBOX_OPEN_COOLDOWN |
Seconds to ignore a second opening after a successful one (default 3, 0 disables it) |
WIBOX_OPEN_SETTLE |
Seconds the video waits after a successful opening, because the bell keeps the single session busy that long (default 75, 0 disables it) |
WIBOX_TRICKLE_MS |
Pace in milliseconds of the filler that keeps the stream published while the tunnel is being established (default 20, 0 disables it) |
WIBOX_LOCK_WAIT |
Seconds a new video process waits for the single-session lock instead of failing at once (default 300, 0 fails immediately) |
WIBOX_WARM_SECONDS |
Seconds the live view stays warm after the last viewer, so the next opening joins the tunnel already established (default 0, off; the add-on sets 180) |
WIBOX_WARM_API |
Local go2rtc API the warm window polls to count viewers (default http://127.0.0.1:1985) |
WIBOX_WARM_RTSP |
Local RTSP URL the warm guardian attaches to (default rtsp://127.0.0.1:8556/wibox) |
WIBOX_VIDEO_RETRY |
Base wait, in seconds, between video reconnects before the backoff doubles (default 5) |
WIBOX_VIDEO_RETRY_MAX |
Ceiling of the video reconnect backoff, in seconds (default 60) |
WIBOX_VIDEO_START_TIMEOUT |
Seconds go2rtc waits for the video producer before killing it (default 300) |
WIBOX_HA_URL |
Home Assistant base URL for the automatic camera entry (empty disables it) |
WIBOX_HA_TOKEN |
Long-lived Home Assistant token used to create the camera entry |
WIBOX_HA_STREAM |
RTSP URL that Home Assistant should record for the camera (empty disables it) |
WIBOX_DEBUG |
Any value enables HTTP and MQTT traces |
A configuration file can also be passed with -config <file>. That is how
go2rtc launches it, since a child process does not inherit the systemd
environment:
If you do not know the doorbell IP, the daemon looks for it:
./wibox-daemon -discover
It scans the QUII port (34567 by default) in the /24s of the machine
interfaces, runs the setup against every candidate and, if it finds one, prints
the WIBOX_LAN_ADDR=... line ready to paste into the configuration. It does not
open the door or request video: only the handshake.
Note: the doorbell only answers when the daemon runs on the same network as the doorbell. From another network the scan finds nothing, and that is not a daemon failure.
The doorbell has a speaker, but only on its local network: the cloud tunnel carries video and the audio from the bell, never audio towards it.
ffmpeg -i mensaje.wav -ar 8000 -ac 1 -f wav -c:a pcm_s16le /tmp/mensaje8k.wav
WIBOX_LAN_ADDR=192.168.1.60:34567 ./wibox-daemon -talk /tmp/mensaje8k.wavThe file has to be PCM16, mono, 8 kHz. The daemon opens a talk session on the same TCP port used for video, sends the audio in 40 ms frames and closes.
./wibox-daemon -check
It logs in, lists the devices and requests the token. It is the quick way to check whether the credentials are still valid.
./wibox-daemon -version prints the build version and the platform it was
compiled for, which is the fastest way to know what a deployment is running.
The session lives in two duplex connections that can die without an error, and
then every call waits out its timeout. When it happens, /status shows
cloud_ok: false with a timeout in cloud_error and /open returns
rc: -1. The daemon renews the session and retries once on that kind of error,
and restarting the add-on also fixes it.
GET /health no token
GET /devices Authorization: Bearer <token>
GET /open?device=<udid> Authorization: Bearer <token>
GET /open?device=<udid>&wait=0 Authorization: Bearer <token>
GET /status Authorization: Bearer <token>
Add wait=0 to /open and the call returns at once with a 503 when the last
cloud check saw the doorbell asleep, instead of waiting up to four minutes for
it to wake. Useful for automations that cannot block.
/status reports the version, uptime, whether a video session is alive and the
last opening attempt (route, code, error, time). It also carries
video_frames, video_keyframes and video_pending, always present and at
zero when nothing is streaming: the last one counts frames already received
that cannot be used until a keyframe arrives, which is how you tell a stream
that is sending media without a picture from one that is silent. device_online says whether the doorbell was listed as online the last time the daemon asked the cloud, and devices_checked is when that was.
Examples:
curl http://192.168.1.50:8087/health
curl -H "Authorization: Bearer $WIBOX_API_TOKEN" http://192.168.1.50:8087/devices
curl -H "Authorization: Bearer $WIBOX_API_TOKEN" \
"http://192.168.1.50:8087/open?device=23123csfewyk"
The doorbell only accepts one QUII session at a time. When go2rtc is already
streaming, the video process owns it, so the API asks that process to open the
door through a local unix socket instead of starting a second tunnel (which
would only get a BUSY answer). If no stream is running, /open builds its own
tunnel as usual.
The daemon does not implement RTSP. It writes raw H.264 Annex-B to stdout and lets go2rtc do the packaging, which is already the streaming engine inside Home Assistant (2024.11 and later).
exec: with the pipe transport reads exactly that: a raw H.264/H.265
bitstream on stdout.
# deploy/go2rtc.yaml
streams:
wibox: exec:/usr/local/bin/wibox-daemon -config /etc/wibox-daemon.conf -video#killsignal=2#killtimeout=5
api:
listen: "127.0.0.1:1984"
rtsp:
listen: "127.0.0.1:8554"Then add it to Home Assistant as a generic camera pointing at
rtsp://127.0.0.1:8554/wibox, or as a go2rtc stream for WebRTC. Two caveats:
- Every
-videorun is one session against the doorbell. With the doorbell asleep, the process spends its time in thep2pconnectretry loop, so it is better to open the stream on demand (a dashboard tap) than to leave it always-on. - Audio is G.711A (PCMA). With
WIBOX_MPEGTS=1the daemon writes MPEG-TS with both tracks on stdout (H.264 with stream type0x1B, A-law with0x90), which is what the add-on uses: go2rtc maps0x90to its PCMA codec. Without the flag-video-audiowrites the audio to stderr and a secondffmpegsource would be needed, because go2rtc treats twoexec:sources as failover, not as a merge. The sound does reach the viewer since 0.10.38 and 0.10.39:ffprobeagainst the add-on's RTSP printspcm_alawfor the second track, and decoding it yields 640,036 samples of non silent audio (the daemon logsaudio=3998, about fifty 20 ms frames per second). One detail for debugging captures: this muxer declares A-law as stream type0x90, while the PMT that go2rtc serves rewrites that track as0x06, so a capture taken on the RTSP side shows a different type than the one written here, and both describe the same PCMA audio.
Home Assistant no longer takes a generic camera in YAML and the config flow
validates by opening the stream, so the entry can only be created once the
stream carries a keyframe. deploy/camera-creation.md has the exact calls,
including the mandatory advanced block.
The daemon creates the entry by itself when it has WIBOX_HA_URL,
WIBOX_HA_TOKEN and WIBOX_HA_STREAM (in the add-on, the ha_url, ha_token
and ha_stream options). It checks /status every minute and creates the
entry if it is missing, without anything else running.
Two paths, and it tries both: with stream_source once the stream delivers
media, and without it, where the flow cannot validate and instead asks for
confirmation (confirmed_ok), which creates the entry backed by the still
image. The second path is what works while the doorbell sends no keyframe, so
the camera is there from the start and only the picture waits.
deploy/create-camera-when-ready.sh is still there as a fallback, and mostly
for its RTSP pull: while it waits it keeps a short ffmpeg pull alive (unless
you set PULL=0), because without a consumer go2rtc never starts the producer
and the bell has nobody to send frames to.
Home Assistant OS cannot run a bare binary, so the daemon also ships as an add-on. It runs inside HAOS, talks to the vendor cloud and exposes the HTTP API on port 8087, which means the door can be opened from Home Assistant without the phone and without being on the building network.
Settings -> Add-ons -> Add-on store -> Repositories
add https://github.com/gnacho/wibox-daemon
install "Wibox Doorbell"
Options, all of them adjustable from the add-on page: account, password,
client id, device id, opening pin and API token; attempts, awake_poll,
video_wait, video_max, play_resends, cloud_check, open_cooldown,
video_retry, video_retry_max, remote_budget, video_start_timeout and
warm_seconds for the timings. The add-on forces the remote chain, so no LAN
address is configured.
From Home Assistant:
rest_command:
wibox_abrir:
url: "http://homeassistant.local:8087/open?device=<device_id>"
method: GET
headers:
Authorization: "Bearer <api_token>"
timeout: 300
wibox_abrir_ya:
url: "http://homeassistant.local:8087/open?device=<device_id>&wait=0"
method: GET
headers:
Authorization: "Bearer <api_token>"
timeout: 30The first command waits: if the doorbell is asleep the daemon keeps retrying
until it gets a peer and only then opens, so the timeout is generous and
attempts bounds that wait. The second one answers straight away with 503 when
the last cloud check saw the bell offline, which is what an automation wants.
open_cooldown keeps a double press from opening twice.
The add-on ships its own go2rtc, so the
camera works without installing anything else. go2rtc is the main process and
the daemon is its exec: source, which means the video session is only started
when someone watches the camera and is killed afterwards.
rtsp://<haos-ip>:8556/wibox
Add it in Home Assistant as a generic camera (RTSP) or through the go2rtc
integration. starttimeout comes from the video_start_timeout option (300 s
by default) because the doorbell sleeps and may take minutes to answer.
With warm_seconds (180 by default) the live view stays warm for that many
seconds after the last viewer: while somebody watches, the daemon keeps one
extra consumer on the local RTSP, and it drops it only once the stream has been
alone for that long. A second opening inside the window joins the tunnel that is
already up, so it shows a picture in about a second instead of paying natcheck,
p2pconnect and the first keyframe again. The price is that the doorbell keeps
its single QUII session during the window, so the phone app and the neighbours
see it busy until it is released. Set it to 0 to go back to a cold start every
time, or to a smaller number to free the doorbell sooner.
docs/building-verification.md is the script for the visit: discover the
doorbell, check the cloud, open the door and read the errors. The real opening
can only be tested on the same network as the doorbell.
Everything the daemon knows is in /status. These are the usual readings:
| What you see | What it means | What to do |
|---|---|---|
device_online: false |
The cloud has not seen the doorbell since devices_checked. It runs on batteries. |
Wait for movement at the portal, or open with ?wait=0 and handle the immediate 503. |
cloud_ok: false with cloud_error |
Login to the manufacturer cloud failed. | Check WIBOX_ACCOUNT/WIBOX_PASSWORD and that the gateway answers. |
video_pending growing, video_keyframes: 0 |
Media is arriving, the picture is waiting for a keyframe. The stream is alive. | Nothing to fix: the doorbell decides when to send an IDR. |
video_lock_free: false |
Another video process on this machine holds the lock (/tmp/wibox-video.lock), so the local session is its. |
Nothing to fix: the lock is doing its job. |
video_lock_pid set |
That is the process inside the container holding the lock; with the add-on it is normally the -video -video-audio child go2rtc spawned. |
Compare it with docker exec <container> ps before killing anything. |
video_session: true, counters at zero |
A player is connected but the bell has not sent anything yet. | Keep watching; it will fill up when the bell wakes. |
last_open.rc: 0 |
The door opened. | Nothing. |
last_open.rc: -10029 |
The opening PIN was rejected. | Fix WIBOX_UNLOCK_PASSWORD. |
| Call answered with 429 | The cooldown is running: the last opening succeeded a moment ago. The body carries retry_after_s, the seconds left. |
Wait that long; the second press is ignored on purpose. |
last_open.rc: -1 with a p2pconnect timeout |
The bell went back to sleep before the tunnel was ready. | Try again in a waking window. |
See deploy/wibox-daemon.service (systemd) and deploy/haos-wibox.yaml (Home
Assistant). The daemon does not need root: it listens on a high port and only
talks outwards.
Health checks, the shipping procedure and the known limits live in docs/operations.md.
The full wire protocol is in docs/protocol.md. Summary of
what this daemon implements:
- userauth duplex:
GET /auth/nologin;jus_duplex=downonly to get thejsessionidcookie,POST /auth/nologin;jus_duplex=upwith the XML envelope, and thenGET .../downto collect the queued response. The order matters: if the long-poll is in flight when the upload arrives, the gateway answersCAUTHNETClient::Response(packet): BeginResponseGenerator return failed. - XStream envelope:
contentbeforeheader, kebab-case fields, emptyversion, 3-space indentation. - QUII over TCP: 32-byte setup with plaintext
0xA9, PLAY with useradminapp2, and transparent commands with type0xFE. AES-256-CBC with a zero IV and SHA-256 of (plaintext header + body) appended to the body. - Opening: transparent command 4, body with lock number, channel, and the hexadecimal SHA-256 of the pin from byte 16.
go test ./...
22 tests: byte-for-byte XStream envelope against the official client format,
parsing of real responses captured from the gateway, AES-256-CBC vector computed
separately with openssl, QUII header layout, SHA-256 tail of the body, opening
body (command 4, pin at byte 16), full opening over a loopback socket that acts
as the doorbell, password rejection, setup with a bad status, LAN discovery, and
the /open HTTP contract (token required, 200 with ok, 502 with the error
code).
Every release with its notes is listed in CHANGELOG.md.
- wibox-android: Android app (Kotlin)
for the same ecosystem. The protocol notes live in its
docs/protocol.md.
GNU Affero General Public License v3.0. See LICENSE.