Files

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-webui on 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.

Leave username/password blank only if anonymous access is permitted. Credential values are literal; % and # within passwords 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

  1. Disconnect the native phone app.
  2. Turn Phone App Mode OFF. Confirm WebUI starts and HA reports OFF.
  3. Turn Phone App Mode ON. Confirm WebUI stops and HA reports ON.
  4. Connect the native phone app and test repeater admin without resetting the companion or repeater.
  5. Disconnect the native app before switching OFF to restore WebUI.
  6. 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.

References