Summary
The MQTT bridge hardcodes port 8080 when connecting to the Meticulous machine. My machine serves its API through nginx on port 80 and has nothing listening on 8080, so the bridge can never connect.
This is not just a telemetry problem: the failed connection makes the bridge publish availability=offline, which the server treats as the machine being offline. The result is a permanent "machine offline" state in the UI and every machine command failing with HTTP 409.
Environment
What happens
The bridge builds its base URL with a hardcoded port:
# meticulous-addon/rootfs/usr/bin/run.py:294
base_url = f"http://{self.machine_ip}:8080"
There is no environment variable or UI setting to override this — METICULOUS_IP accepts only a host, never a port.
Resulting log loop:
run - WARNING - Socket.IO connection failed: HTTPConnectionPool(host='10.0.30.52', port=8080):
Max retries exceeded with url: /https://www.google.com/url?q=http://socket.io/?transport%3Dpolling%26EIO%3D4&source=gmail&ust=1785552303889000&sa=E
(Caused by NewConnectionError(... [Errno 111] Connection refused))
run - INFO - Connectivity state: disconnected
run - WARNING - Socket reconnection failed (attempt 12) ...
/api/v1/profile/list fails the same way.
Why it cascades
bridge cannot reach <machine>:8080
└─> publishes availability=offline on MQTT
└─> server/api/routes/commands.py:73 _require_connected()
└─> HTTP 409 "Machine is offline" on every command
└─> home page shows the machine as offline
So the port assumption breaks live status, live telemetry, Home Assistant discovery, and all machine control.
Notably, the server component gets this right — it addresses the machine without an explicit port (http://{ip}/api/v1/...), so shot history, shot analysis and profile generation all work correctly against the same machine. Only the components that append :8080 fail.
Evidence that the machine only serves port 80
From the Metic container:
$ curl -o /dev/null -w '%{http_code}' https://www.google.com/url?q=http://10.0.30.52:8080/api/v1/machine&source=gmail&ust=1785552303889000&sa=E
000 # connection refused
$ curl -o /dev/null -w '%{http_code}' https://www.google.com/url?q=http://10.0.30.52/api/v1/machine&source=gmail&ust=1785552303889000&sa=E
200
$ curl -I https://www.google.com/url?q=http://10.0.30.52/api/v1/machine&source=gmail&ust=1785552303889000&sa=E
HTTP/1.1 405 Method Not Allowed
Server: nginx/1.22.1
Access-Control-Allow-Origin: *
$ curl https://www.google.com/url?q=http://10.0.30.52/api/v1/machine&source=gmail&ust=1785552303889000&sa=E
{"name": "MeticulousDashingRiceMilk", "firmware": "0.2.24-372-g7aa359b", ...}
Socket.IO is served on port 80 as well and handshakes correctly there:
$ curl 'https://www.google.com/url?q=http://10.0.30.52/socket.io/?transport%3Dpolling%26EIO%3D4&source=gmail&ust=1785552303889000&sa=E'
0{"sid":"1PWZJDotLBBCeTPgAAAH","upgrades":["websocket"],"pingTimeout":60000,...}
Steps to reproduce
- Have a Meticulous machine on firmware
0.2.24 that serves its API via nginx on port 80.
- Install Metic v2.6.0 and set
METICULOUS_IP to the machine's address.
- Open the home page — the machine shows as offline, permanently.
docker logs meticai — Socket.IO reconnection failures against :8080, retrying forever.
- Any machine command returns
409 Machine is offline.
Other places with the same assumption
server/services/machine_discovery_service.py:206
response = await client.get(f"http://{ip}:8080/api/getLastShotProfileJSON")
This presumably also prevents network auto-discovery from finding machines on this firmware. (I did not test discovery directly — I configured the IP manually.)
Suggested fix
Make the machine port configurable rather than hardcoded, e.g. a METICULOUS_PORT env var flowing into machine_port in options.json (bridge/start_bridge.py already maps METICULOUS_IP → machine_ip), defaulting to 8080 so existing setups are unaffected.
Alternatively, probe both ports on startup and use whichever answers — that would fix it with no user configuration, which matters because the failure is silent from the UI's perspective: it just says "offline" with no hint that a port is involved.
It would also help if the UI surfaced why the machine is considered offline, instead of only the final state.
Workaround (for anyone hitting this)
A TCP forwarder in front of the machine that answers on both ports, with METICULOUS_IP pointed at it:
services:
metic-machine-proxy:
image: alpine/socat:latest
container_name: metic-machine-proxy
restart: unless-stopped
entrypoint: ["/bin/sh", "-c"]
command:
- |
socat TCP-LISTEN:80,fork,reuseaddr TCP:${METICULOUS_MACHINE_IP}:80 &
socat TCP-LISTEN:8080,fork,reuseaddr TCP:${METICULOUS_MACHINE_IP}:80 &
wait -n
METICULOUS_MACHINE_IP=<real machine IP>
METICULOUS_IP=metic-machine-proxy
Plain TCP forwarding, so the Socket.IO websocket upgrade passes through intact. With this in place the bridge connects immediately, availability goes to online, and commands work:
run - INFO - Connected to MeticulousDashingRiceMilk (Serial: 013116)
run - INFO - Connectivity state: connected
Happy to test a patch against this firmware if that's useful.
Summary
The MQTT bridge hardcodes port
8080when connecting to the Meticulous machine. My machine serves its API through nginx on port 80 and has nothing listening on 8080, so the bridge can never connect.This is not just a telemetry problem: the failed connection makes the bridge publish
availability=offline, which the server treats as the machine being offline. The result is a permanent "machine offline" state in the UI and every machine command failing with HTTP 409.Environment
0.2.24-372-g7aa359b(software_version2026-07-28 00:56:57)nginx/1.22.1on port 80What happens
The bridge builds its base URL with a hardcoded port:
There is no environment variable or UI setting to override this —
METICULOUS_IPaccepts only a host, never a port.Resulting log loop:
/api/v1/profile/listfails the same way.Why it cascades
So the port assumption breaks live status, live telemetry, Home Assistant discovery, and all machine control.
Notably, the server component gets this right — it addresses the machine without an explicit port (
http://{ip}/api/v1/...), so shot history, shot analysis and profile generation all work correctly against the same machine. Only the components that append:8080fail.Evidence that the machine only serves port 80
From the Metic container:
Socket.IO is served on port 80 as well and handshakes correctly there:
Steps to reproduce
0.2.24that serves its API via nginx on port 80.METICULOUS_IPto the machine's address.docker logs meticai— Socket.IO reconnection failures against:8080, retrying forever.409 Machine is offline.Other places with the same assumption
This presumably also prevents network auto-discovery from finding machines on this firmware. (I did not test discovery directly — I configured the IP manually.)
Suggested fix
Make the machine port configurable rather than hardcoded, e.g. a
METICULOUS_PORTenv var flowing intomachine_portinoptions.json(bridge/start_bridge.pyalready mapsMETICULOUS_IP → machine_ip), defaulting to8080so existing setups are unaffected.Alternatively, probe both ports on startup and use whichever answers — that would fix it with no user configuration, which matters because the failure is silent from the UI's perspective: it just says "offline" with no hint that a port is involved.
It would also help if the UI surfaced why the machine is considered offline, instead of only the final state.
Workaround (for anyone hitting this)
A TCP forwarder in front of the machine that answers on both ports, with
METICULOUS_IPpointed at it:Plain TCP forwarding, so the Socket.IO websocket upgrade passes through intact. With this in place the bridge connects immediately,
availabilitygoes toonline, and commands work:Happy to test a patch against this firmware if that's useful.