- Go 98.4%
- Makefile 1.1%
- Dockerfile 0.5%
| .forgejo/workflows | ||
| cmd/wifipool-to-mqtt | ||
| deploy | ||
| docs | ||
| internal | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
WifiPoolToMQTT
Publishes the readings of a Beniferro WifiPool (Gen 1) chlorine and redox system over MQTT — pH, redox/ORP, temperature, flow, tank level and relay states.
The Gen 1 hardware has no local API. The vendor app talks to the WifiPool cloud, and so does this tool: it logs in with your app credentials, subscribes to the cloud's realtime feed and republishes each change on your own MQTT broker. With MQTT discovery enabled, the entities appear in Home Assistant on their own.
The cloud API is undocumented. Every endpoint was reconstructed from the app's traffic. Beniferro does not announce changes, so an update on their side can break this tool at any time. Credit for the original reverse engineering goes to mruiter and the Homey app
com.wifipool.
Features
- Read-only — the tool never writes to the pool controller.
- Realtime: values are pushed from the cloud the moment they change, so a pump that only runs for a few seconds is caught — not missed between polls.
- Each reading on its own topic and as one JSON document per device.
- Home Assistant MQTT discovery, including per-entity availability.
- Stale detection: a reading that stops updating turns the entity unavailable instead of showing an outdated value forever.
- Works with a local broker or a hosted one — plain MQTT, TLS, or mutual TLS.
- Fully configured through a
.envfile; secrets can come from files. - A health endpoint and, on request, Prometheus metrics.
Quick start
You need three things: your WifiPool app login, a running MQTT broker, and Docker (or Podman).
1. Create a folder with a .env file:
# WifiPool cloud — the same login you use in the vendor app
WIFIPOOL_EMAIL=you@example.com
WIFIPOOL_PASSWORD=your-app-password
# Your MQTT broker. Use a real hostname or IP — not 0.0.0.0 or localhost,
# which from inside a container point at the container itself.
MQTT_BROKER_URL=tcp://192.168.1.10:1883
#MQTT_USERNAME=
#MQTT_PASSWORD=
2. Check that the tool finds your system (reads the cloud only, touches no broker):
docker run --rm --env-file .env \
gbjo.mazurczak.cloud/nexus/wifipooltomqtt:latest discover
You should see a table per device listing pH, redox, flow and so on with their last values. If a value looks wrong, see Fixing the channel mapping.
3. Run it:
docker run -d --name wifipool-to-mqtt --restart unless-stopped \
--env-file .env \
gbjo.mazurczak.cloud/nexus/wifipooltomqtt:latest
That is it. The readings are now on your broker under wifipool/…, and in Home
Assistant a WifiPool device appears by itself. Watch the log with
docker logs -f wifipool-to-mqtt.
Prefer Compose? See Running with Docker Compose.
Fixing the channel mapping
The cloud names each channel ("pH", "Redox", "Flow pH"), and the tool uses those
names. For channels it cannot identify it guesses from the value range, and a
guess can be wrong. The discover command shows what it decided:
Device 6f6a1234-5678-4abc-8def-0123456789ab (Pool)
ROLE PORT CLOUD NAME IO KIND LAST VALUE SOURCE
ph o0 pH a686261c-….o0 analog 7.196 metadata
flow o1 Flow pH a686261c-….o1 switch true metadata
level o2 Level pH a686261c-….o2 switch false metadata
SOURCE tells you where the role came from: metadata is the cloud's own name
(reliable), probe is a guess, override is your own setting.
If a role sits on the wrong channel, pin it in .env with the IO value from
the table and restart:
WIFIPOOL_IO_PH=6f6a1234-5678-4abc-8def-0123456789ab.o0
The override variables are WIFIPOOL_IO_PH, _REDOX, _TEMPERATURE, _FLOW,
_LEVEL and _RELAYS. An override always wins.
Topics
Each reading is published on its own topic and — retained — as one JSON
document. <dev> is the short device id (first block of its UUID).
wifipool/status online | offline (last will)
wifipool/<dev>/availability online | offline
wifipool/<dev>/ph 7.21
wifipool/<dev>/redox 680
wifipool/<dev>/temperature 24.3
wifipool/<dev>/flow ON | OFF
wifipool/<dev>/level ON | OFF
wifipool/<dev>/relay/o0 ON | OFF
wifipool/<dev>/<entity>/availability online | offline
wifipool/<dev>/state JSON (see below)
{
"device_id": "6f6a1234",
"domain": "2ca59577-…",
"healthy": true,
"polled_at": "2026-07-22T14:31:05Z",
"ph": { "value": 7.21, "ts": "2026-07-22T14:31:02Z", "stale": false },
"redox": { "value": 680, "ts": "2026-07-22T14:31:02Z", "stale": false },
"flow": { "value": true, "ts": "2026-07-22T14:29:58Z", "stale": false }
}
To watch what arrives, subscribe with any MQTT client — for example from a Mosquitto container, so you need nothing installed:
docker run --rm -it docker.io/library/eclipse-mosquitto:2 \
mosquitto_sub -h YOUR_BROKER -v -t 'wifipool/#'
Realtime updates
By default the tool keeps a realtime connection to the WifiPool cloud and publishes every change the moment the controller reports it. This matters for the pumps: a dosing pump often runs in short bursts of a few seconds, which a periodic poll would step right over. The pumps are streamed at the fastest rate the hardware offers (about one state report every ten seconds), so their on/off transitions land in MQTT — and Home Assistant — within seconds.
While realtime is active, the tool still calls the cloud's history endpoint
occasionally as a safety net (WIFIPOOL_REALTIME_BACKFILL_INTERVAL, default
15 min). A reading counts as unavailable once it has had no update for
WIFIPOOL_REALTIME_STALE_AFTER (default 5 min).
| Variable | Default | Meaning |
|---|---|---|
WIFIPOOL_REALTIME_ENABLED |
true |
Set false to fall back to pure polling on WIFIPOOL_POLL_INTERVAL. |
WIFIPOOL_REALTIME_STALE_AFTER |
5m |
No update for this long → entity unavailable. |
WIFIPOOL_REALTIME_PUBLISH_INTERVAL |
1s |
A burst of changes is coalesced into at most one publish per interval. |
WIFIPOOL_REALTIME_BACKFILL_INTERVAL |
15m |
How often the history endpoint is polled as a safety net (0 disables it). |
WIFIPOOL_REALTIME_ACTIVE_DEVICES |
false |
Also fast-stream the pH/redox sensors (pumps are always fast-streamed). |
The pH and redox sensors change slowly, so their normal ~35 s heartbeat is used
unless you turn on WIFIPOOL_REALTIME_ACTIVE_DEVICES for near-continuous
analog updates (at the cost of more cloud traffic).
Home Assistant
With HA_DISCOVERY_ENABLED=true (the default), one device per pool controller
appears automatically with sensors for pH, redox, temperature, flow, level, the
relays and a "last update" timestamp. Point the Home Assistant MQTT integration
at the same broker and the entities show up under Settings → Devices & Services
→ MQTT — no YAML needed.
Entities become unavailable when the tool stops, the device is unreachable, or a reading goes stale, so Home Assistant never shows a frozen value as if it were current.
If your Home Assistant uses a non-default discovery prefix, set
HA_DISCOVERY_PREFIX to match.
Running with Docker Compose
# compose.yaml
services:
wifipool-to-mqtt:
image: gbjo.mazurczak.cloud/nexus/wifipooltomqtt:latest
container_name: wifipool-to-mqtt
restart: unless-stopped
env_file:
- .env
ports:
# Optional — only needed to reach the health/metrics endpoint from
# outside. Change the left number if 8080 is taken on the host.
- "${HEALTH_PORT:-8080}:8080"
healthcheck:
test: ["CMD", "wifipool-to-mqtt", "healthcheck"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 3
docker compose up -d
docker compose logs -f
Update to a newer image with:
docker compose pull && docker compose up -d
The tool keeps no state — it rediscovers and republishes everything on start.
Connecting to your broker
MQTT_BROKER_URL accepts:
| Scheme | Use |
|---|---|
tcp://host:1883 |
plain MQTT |
ssl://host:8883 |
MQTT over TLS |
ws:// / wss:// |
MQTT over WebSockets |
For a broker that needs a username and password, set MQTT_USERNAME and
MQTT_PASSWORD. Examples for common hosted brokers:
HiveMQ Cloud / EMQX Cloud (TLS with username and password):
MQTT_BROKER_URL=ssl://xxxxx.s1.eu.hivemq.cloud:8883
MQTT_USERNAME=wifipool
MQTT_PASSWORD=your-broker-password
AWS IoT Core (mutual TLS on port 443):
MQTT_BROKER_URL=ssl://xxxxxxxx-ats.iot.eu-central-1.amazonaws.com:443
MQTT_TLS_CERT_FILE=/certs/device.pem.crt
MQTT_TLS_KEY_FILE=/certs/private.pem.key
MQTT_TLS_CA_FILE=/certs/AmazonRootCA1.pem
MQTT_TLS_ALPN=x-amzn-mqtt-ca
Sharing one broker with other tools
Put every topic under a prefix so it cannot collide with anything else:
MQTT_TOPIC_PREFIX=pool
MQTT_CLIENT_ID=wifipool-to-mqtt-pool # must be unique per instance
All topics then live under pool/…, which also matches a broker ACL of the form
pool/#. Give each instance its own MQTT_CLIENT_ID — a broker disconnects an
existing session when another client reconnects with the same id.
Configuration
Everything is set through the .env file. Only three values are required;
everything else has a sensible default. The most useful ones:
| Variable | Default | Meaning |
|---|---|---|
WIFIPOOL_EMAIL / WIFIPOOL_PASSWORD |
— | required — your WifiPool app login |
MQTT_BROKER_URL |
— | required — your broker, e.g. tcp://192.168.1.10:1883 |
MQTT_USERNAME / MQTT_PASSWORD |
— | broker credentials, if any |
MQTT_TOPIC_PREFIX |
(none) | prefix in front of every topic |
MQTT_CLIENT_ID |
wifipool-to-mqtt |
must be unique per instance |
WIFIPOOL_REALTIME_ENABLED |
true |
realtime cloud feed (see Realtime updates) |
WIFIPOOL_POLL_INTERVAL |
60s |
poll interval when realtime is disabled (minimum 15s) |
HA_DISCOVERY_ENABLED |
true |
announce entities to Home Assistant |
LOG_LEVEL |
info |
debug, info, warn, error |
HEALTH_PORT |
8080 |
host port for the health endpoint (compose) |
METRICS_ENABLED |
false |
expose Prometheus metrics at /metrics |
The full list with every option and its default is in
.env.example. Any secret can be read from a file instead by
appending _FILE, e.g. MQTT_PASSWORD_FILE=/run/secrets/mqtt_password.
Health and metrics
GET /healthz— the process is alive.GET /readyz—200only when the broker is connected and a poll recently succeeded; otherwise503with a JSON reason.GET /metrics— Prometheus metrics, only whenMETRICS_ENABLED=true.
These listen on port 8080 inside the container. With the Compose file above,
reach them at http://localhost:${HEALTH_PORT}/readyz.
Troubleshooting
| Symptom | What it means / what to do |
|---|---|
connection refused to 0.0.0.0 or 127.0.0.1 |
Those addresses point at the container itself. Use the broker's real IP or hostname in MQTT_BROKER_URL. On the same Docker host, use the host's LAN IP or the bridge gateway 172.17.0.1, or put both containers on one Docker network and use the broker's container name. |
failed to bind host port … :8080: address already in use |
Something else on the host uses 8080. Set HEALTH_PORT to a free port, or remove the ports: line — the healthcheck works without it. |
| A value is on the wrong sensor (e.g. pH shows 680) | The mapping guessed wrong. Run discover and pin it with WIFIPOOL_IO_*. See Fixing the channel mapping. |
| An entity is permanently unavailable | That channel stopped reporting. If a slow sensor ages out, raise WIFIPOOL_REALTIME_STALE_AFTER (or WIFIPOOL_STALE_OFFSET when realtime is disabled). |
| Nothing arrives at the broker | Wrong credentials or a topic the broker rejects. Set LOG_LEVEL=debug and check the startup log. |
| Two instances keep disconnecting each other | They share MQTT_CLIENT_ID. Give each its own. |
For more detail, run with LOG_LEVEL=debug — it logs every cloud request and
every publish, with passwords and session cookies masked.
Building from source
Requires Go (see go.mod for the version).
make build # build the binary into bin/
make test # run the tests
make docker-build # build the container image locally
make discover # run discovery against the cloud using your .env
make help # list all targets
Licence
MIT — see LICENSE.