295 lines
12 KiB
Markdown
295 lines
12 KiB
Markdown
# 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`.
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```text
|
|
/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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```json
|
|
{"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.
|
|
|
|
```ini
|
|
[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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
sudo docker stop meshcore-webui
|
|
```
|
|
|
|
After disconnecting the native phone app:
|
|
|
|
```bash
|
|
sudo docker start meshcore-webui
|
|
```
|
|
|
|
To disable the controller:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
- [MeshCore WebUI](https://github.com/adradr/meshcore-webui)
|
|
- [Home Assistant MQTT Switch](https://www.home-assistant.io/integrations/switch.mqtt/)
|
|
- [Home Assistant packages](https://www.home-assistant.io/docs/configuration/packages/)
|
|
- [Eclipse Paho MQTT client API](https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html)
|