12 KiB
MeshCore Phone App Mode
An MQTT switch for handing a MeshCore companion radio between MeshCore WebUI and the native MeshCore phone app.
MeshCore WebUI holds a persistent TCP connection from its backend, even when all browser tabs are closed. On a companion that only accepts one direct client, this can prevent a phone app from connecting or completing repeater admin requests. This controller temporarily stops the WebUI container and restarts it when you have finished with the phone app.
Version 1.1, by Zorruno, 2026-10-03. Initially deployed and confirmed working on Debian 12 with Python 3.11, Eclipse Mosquitto, Docker, MeshCore WebUI and Home Assistant. Licensed under MIT.
What the switch means
| Phone App Mode | WebUI container | What to do |
|---|---|---|
| ON | Stopped | Connect the native phone app to the companion |
| OFF | Running | Use WebUI from your browser |
Disconnect the native phone app before switching OFF. Stop any other software that connects directly to this companion too.
The controller runs as a separate systemd service on the Docker host. It:
- Accepts only plain ON/OFF commands on the MQTT topic configured in your private INI.
- Starts/stops only the container named
meshcore-webui. - Inspects actual Docker state and reports retained JSON every 15 seconds.
- Publishes online/offline availability, with an MQTT Last Will.
- Reconnects to MQTT without changing container state.
- Ignores retained commands received when subscribing.
- Performs no start/stop action on controller, broker or Home Assistant restart.
Home Assistant waits for confirmed state instead of assuming a command worked. No SSH setup or Docker API connection from Home Assistant is required.
This is a connection handover, not a repeater watchdog. A running WebUI container does not prove that it connected to the radio. A stopped WebUI container does not prove that every other client disconnected or that a remote repeater is reachable.
Files
| File | Purpose |
|---|---|
controller.py |
MQTT listener, Docker start/stop and state reporting |
meshcore-phone-mode.service |
Independent systemd service |
install.sh |
Debian/Ubuntu installation helper |
meshcore-phone-mode.ini.example |
Broker settings and full topic names, with blank credentials |
meshcore_phone_app_mode.yaml |
Complete Home Assistant package |
requirements.txt |
Pinned Python MQTT dependency |
test_controller.py |
Behaviour tests with mocked MQTT and Docker |
CHANGELOG.md |
Version history |
Requirements
- A Debian/Ubuntu Docker host using systemd, with Python 3 and internet access for installation packages.
- A container named
meshcore-webuion that host. - An MQTT broker reachable by both this controller and Home Assistant, or an existing bridge carrying the topics below.
- Home Assistant's MQTT integration and an existing packages configuration.
The service runs as root to control the local Docker daemon, but MQTT payloads cannot select arbitrary commands or container names. Only the exact ON/OFF commands are accepted, and Docker is called using separate argument values. Keep access to the MQTT command topic limited to clients that should control WebUI. Do not include your real broker configuration in Git.
Installation on the Docker host
Clone this repository using its clone URL from Gitea. A suitable working
location is /dockervolumes/meshcore-phone-mode.
git clone https://home.fox.co.nz/gitea/zorruno/meshcore-phone-mode.git /dockervolumes/meshcore-phone-mode
cd /dockervolumes/meshcore-phone-mode
sudo bash install.sh
sudo nano /etc/meshcore-phone-mode.ini
The installer installs Python venv support and paho-mqtt 2.1.0, puts the runtime
in /opt/meshcore-phone-mode, creates a root-only example config at
/etc/meshcore-phone-mode.ini, and installs the systemd service. It does not
start the service or change the WebUI container. It preserves an existing
config file. A rerun validates that config before stopping an active controller
or replacing its runtime. If the config is invalid, the existing controller is
left running. A valid update leaves the service stopped until you start it.
Configure the same broker Home Assistant already uses. If it runs on this
Docker host with host port 1883 published, use 127.0.0.1:1883. Otherwise enter
the existing broker's address and port. Each machine's localhost is its own
machine. Do not change HA's existing MQTT broker just to add this switch.
Enter username/password and all three full MQTT topics locally in the INI
file, without surrounding quotes. The shared example uses generic topic names;
replace them with your own if desired. The three topics are required, must be
different, and cannot contain + or # wildcards. There is no hidden prefix
or fallback to the original installation's topics. Set a unique client_id
if several controllers share a broker.
Enter values without surrounding quotes.
Leave both blank only if anonymous access is permitted. Values are literal;
% and # within values are supported. Credentials are not passed as CLI
arguments and are not included in this repository.
The private INI is created with permissions 0600 and is excluded from Git.
Home Assistant topic settings must also stay in your local configuration.
Topic names are configuration, not passwords: MQTT authentication and broker
permissions still control who can use them.
The broker account needs read access to the command topic and write access to the state and availability topics. A denied subscription keeps the controller unavailable until permissions are corrected and the service is restarted.
Start and verify:
sudo systemctl enable --now meshcore-phone-mode.service
sudo journalctl -u meshcore-phone-mode.service -n 30 --no-pager
Expected log: MQTT subscribed; checking current WebUI state. The first state
reflects whether WebUI was already running or stopped; starting the controller
does not change it.
Home Assistant package
Copy meshcore_phone_app_mode.yaml into:
/config/packages/meshcore_phone_app_mode.yaml
Before installing the package, match its topic fields to your private INI:
| Home Assistant field | INI setting in [mqtt] |
|---|---|
command_topic |
command_topic |
state_topic |
state_topic |
json_attributes_topic |
state_topic |
availability_topic |
availability_topic |
Home Assistant does not read the controller's INI. Edit these fields in the local HA package; do not commit that customised package back to this repo.
The supplied file assumes an existing package include such as:
homeassistant:
packages: !include_dir_named packages
Keep the existing include; do not create duplicate homeassistant: or mqtt:
keys in the main configuration file. If you use !include_dir_merge_named,
wrap this file in a top-level package name as described in HA's documentation.
Check the configuration, then restart Home Assistant. Find the switch named
MeshCore Phone App Mode, normally switch.meshcore_phone_app_mode.
The package has a unique ID, so the entity can be renamed in HA. If you deploy
several controllers, give each its own client ID, MQTT topics and HA unique ID.
Check the handover
- Disconnect the native phone app.
- Turn Phone App Mode OFF. Confirm WebUI starts and HA reports OFF.
- Turn Phone App Mode ON. Confirm WebUI stops and HA reports ON.
- Connect the native phone app and test repeater admin without resetting the companion or repeater.
- Disconnect the native app before switching OFF to restore WebUI.
- Restart the controller while mode is ON. WebUI should remain stopped.
Inspect the actual container state at any time:
sudo docker inspect --format '{{.State.Status}}' meshcore-webui
MQTT topics
| Purpose | Topic | Payload | Retained |
|---|---|---|---|
| Command | meshcore/phone_app_mode/set |
Plain ON or OFF | No |
| State | meshcore/phone_app_mode/state |
JSON | Yes |
| Availability | meshcore/phone_app_mode/availability |
online / offline | Yes |
Example state:
{"phone_app_mode":"ON","container":"meshcore-webui","container_status":"exited","observed_at":"2026-10-03T01:30:00+00:00"}
These are generic example topics. Configure the full topic names under [mqtt]
in /etc/meshcore-phone-mode.ini, and use the same values in your local HA
package. Source edits are not needed. The example MQTT client ID is
meshcore-phone-mode; set client_id in the INI to make it unique if needed.
[mqtt]
command_topic = meshcore/phone_app_mode/set
state_topic = meshcore/phone_app_mode/state
availability_topic = meshcore/phone_app_mode/availability
Restart the controller after changing its INI, and reload/restart HA after changing its package. The subscription, state reports, online/offline reports and Last Will all use these configured values.
Never publish retained commands. The controller uses a clean session and clears pending commands on disconnect. QoS 1 duplicates are idempotent once the container is already in the requested state. Commands are not run through a shell, and no command parameters are accepted from MQTT.
Updates, backup and recovery
Upgrading from 1.0: before installing 1.1, add command_topic, state_topic
and availability_topic to the existing [mqtt] section of your private INI.
Use the exact values from your working HA package. Keep that existing HA
package (including its unique_id) unchanged. This preserves your switch and
current topic scheme. Do not replace your private INI with the generic example.
If any topic is missing or invalid, 1.1 refuses to start rather than subscribing
to an unexpected topic.
To update from Git, pull your chosen version, edit the private INI if needed, rerun the installer, then restart:
cd /dockervolumes/meshcore-phone-mode
git pull --ff-only
sudo nano /etc/meshcore-phone-mode.ini
sudo bash install.sh
sudo systemctl start meshcore-phone-mode.service
The installer preserves /etc/meshcore-phone-mode.ini. Back that file up
privately if you need to restore broker credentials and custom topics; Git
contains only its generic example. Keep your customised HA package in your
private HA backup too. The source repository plus that private config are enough
to reinstall the controller. WebUI's own data/config need their normal backup.
Diagnostics:
sudo systemctl status meshcore-phone-mode.service --no-pager
sudo journalctl -u meshcore-phone-mode.service -n 50 --no-pager
If HA shows unavailable, check the controller log, broker credentials and permissions, and whether HA receives the availability topic. Keep optimistic mode disabled so a failed Docker operation is not hidden.
Manual handover:
sudo docker stop meshcore-webui
After disconnecting the native phone app:
sudo docker start meshcore-webui
To disable the controller:
sudo systemctl disable --now meshcore-phone-mode.service
Remove the HA package if desired. Disabling the controller leaves WebUI in its current state. Logs use the system journal, with no separate unbounded log file.
Validation
python3 -m unittest -q test_controller.py
24 tests cover retained/invalid commands, denied subscriptions, reconnects with no container changes, confirmed state reporting, failed stops, duplicate commands, exact container arguments, missing/paused containers and MQTT publication failures, custom topics throughout the controller and Last Will, missing/invalid/duplicate topic settings, and matching shared examples. Tests mock MQTT/Docker and do not change live services. Python syntax, installer shell syntax and YAML settings were checked; the original live installation and HA handover were confirmed working.