diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cb50882 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +## 1.1 — 2026-10-03 + +- Full command, state and availability topics are required in the private INI. +- Subscription, state publication, availability and Last Will use those settings. +- Generic shared topic examples; no installation-specific topic prefixes in the current source. +- MQTT client ID can also be configured in the INI. +- Missing, blank, duplicate, wildcard and oversized topics are rejected at startup. +- Upgrade instructions preserve existing topics and the existing Home Assistant entity. +- 24 behaviour/configuration tests with mocked MQTT and Docker. + +## 1.0 — 2026-10-03 + +- MQTT-controlled handover between MeshCore WebUI and a native phone app. +- Independent systemd controller; stops/starts only `meshcore-webui`. +- Confirmed Docker state, retained JSON and MQTT availability/Last Will. +- Automatic MQTT reconnect without changing container state. +- Non-retained ON/OFF commands and idempotent container operations. +- Home Assistant package, Debian installer and example credentials config. +- 16 behaviour tests; initial installation and switching confirmed working. diff --git a/README.md b/README.md new file mode 100644 index 0000000..27418a5 --- /dev/null +++ b/README.md @@ -0,0 +1,294 @@ +# 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) diff --git a/meshcore-phone-mode.service b/meshcore-phone-mode.service index bd8d3c8..47cc4eb 100644 --- a/meshcore-phone-mode.service +++ b/meshcore-phone-mode.service @@ -1,4 +1,4 @@ -# Zorruno — MeshCore Phone App Mode v1.0 — 2026-10-03 +# Zorruno — MeshCore Phone App Mode v1.1 — 2026-10-03 [Unit] Description=MeshCore WebUI phone-app handover via MQTT After=network-online.target docker.service