No description
  • Go 98.4%
  • Makefile 1.1%
  • Dockerfile 0.5%
Find a file
nexus 6c32d3c596
All checks were successful
CI / test (push) Successful in 2m46s
CI / image (push) Successful in 1m3s
CI / publish-commit-tag (push) Successful in 31s
CI / publish-version-tag (push) Has been skipped
Merge pull request 'Publish realtime analog values with three decimals, not one' (#12) from fix/realtime-analog-precision into main
Reviewed-on: #12
2026-07-31 11:49:37 +02:00
.forgejo/workflows Verify each image push by pulling the tag back 2026-07-24 13:42:44 +02:00
cmd/wifipool-to-mqtt Push live values in realtime over the cloud's socket.io feed 2026-07-30 15:02:01 +02:00
deploy Initial implementation of WifiPoolToMQTT 2026-07-22 16:54:40 +02:00
docs Publish realtime analog values with three decimals, not one 2026-07-31 11:40:05 +02:00
internal Publish realtime analog values with three decimals, not one 2026-07-31 11:40:05 +02:00
.env.example Push live values in realtime over the cloud's socket.io feed 2026-07-30 15:02:01 +02:00
.gitignore Initial implementation of WifiPoolToMQTT 2026-07-22 16:54:40 +02:00
docker-compose.yml Make the published health port configurable 2026-07-23 14:44:21 +02:00
Dockerfile Build through the Podman socket proxy instead of ko 2026-07-23 09:56:31 +02:00
go.mod Push live values in realtime over the cloud's socket.io feed 2026-07-30 15:02:01 +02:00
go.sum Initial implementation of WifiPoolToMQTT 2026-07-22 16:54:40 +02:00
LICENSE Initial implementation of WifiPoolToMQTT 2026-07-22 16:54:40 +02:00
Makefile Build through the Podman socket proxy instead of ko 2026-07-23 09:56:31 +02:00
README.md Push live values in realtime over the cloud's socket.io feed 2026-07-30 15:02:01 +02:00

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 .env file; 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 /readyz200 only when the broker is connected and a poll recently succeeded; otherwise 503 with a JSON reason.
  • GET /metrics — Prometheus metrics, only when METRICS_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.