# MQTT reference

> Topic layout, the state and event payloads, the command list and delivery details for talking to Kiosky over MQTT.

Kiosky speaks plain MQTT, so any system can use it, not only Home Assistant. This page is the reference. For setup and automations see [Home Assistant integration](home-assistant.md).

Kiosky connects as an MQTT 3.1.1 client. Messages are published and subscribed at QoS 1. TLS is optional and set under **Settings, Connections, MQTT**.

## Topics

Everything lives under `<base>/<device_id>`. The base topic defaults to `kiosk`. The device id is a lower-case slug of the device name: letters and digits, with other characters collapsed to `_`, accents removed and at most 40 characters. `Hallway Tablet` gives `hallway_tablet`, so topics start `kiosk/hallway_tablet/`.

| Topic | Direction | Retained | Payload |
|---|---|---|---|
| `kiosk/<id>/availability` | tablet to broker | yes, and the last will | `online` or `offline` |
| `kiosk/<id>/state` | tablet to broker | yes | JSON of all values, see [State](#state) |
| `kiosk/<id>/event` | tablet to broker | no | `{"type": "...", ...}`, see [Events](#events) |
| `kiosk/<id>/camera/snapshot` | tablet to broker | no | JPEG bytes |
| `kiosk/<id>/cmd/<command>` | broker to tablet | n/a | command payload, see [Commands](#commands) |
| `homeassistant/<component>/<id>/<object>/config` | tablet to broker | yes | Home Assistant discovery config |

Retained messages on `cmd/` topics are ignored, so an old command is not replayed when the tablet reconnects. Only direct children of `cmd/` count; `cmd/a/b` is not a command.

Base topic rules: it must be non-empty, have no wildcards (`#`, `+`), no leading `/`, no spaces and be at most 128 characters.

## State

`kiosk/<id>/state` is a flat JSON object. It is published whenever a value changes (with a small minimum gap) and at least every **publish interval** (60 seconds by default, from 5 to 3600 in the config). Unknown values are `null`.

| Key | Type | Meaning |
|---|---|---|
| `screen` | `"ON"` / `"OFF"` | `OFF` only while the screen is off. |
| `brightness` | number | 0 to 255. |
| `screen_stage` | string | `active`, `dimmed`, `screensaver` or `off`. |
| `screensaver` | `"ON"` / `"OFF"` | |
| `battery` | number | Percent. |
| `charging` | boolean | |
| `battery_temp` | number | Degrees C. |
| `light_lux` | number | |
| `proximity` | boolean | |
| `url` | string | Current page. |
| `dashboard` | string | Name of the dashboard on screen. |
| `volume` | number | 0 to 100. |
| `wifi_rssi` | number | dBm. Left out when network info is not shared. |
| `ip` | string | Left out when network info is not shared. |
| `uptime_s` | number | Seconds. |
| `memory_free_mb`, `storage_free_mb` | number | |
| `app_version`, `webview_version` | string | |
| `last_interaction` | ISO time | Last touch. |
| `kiosk_mode` | string | `none`, `soft` or `device_owner`. |
| `charge_wanted` | boolean | Battery care: charger should be on. |
| `memory_trim_count` | number | |
| `motion` | boolean | |
| `kiosk_paused` | boolean | |
| `kiosk_paused_until` | ISO time | `null` when not paused, or paused until resumed. |
| `kiosk_lock` | `"ON"` / `"OFF"` | `OFF` while paused. |
| `presence` | boolean | Only present while face presence is on. |
| `tamper` | boolean | Only present while tamper detection is on. |
| `voice_state` | string | Only while voice is on: `off`, `idle`, `listening`, `processing`, `responding`. |
| `microphone` | `"ON"` / `"OFF"` | Only while voice is on. |
| `voice_last_command` | string | Only while voice is on and sharing the transcript. |

IP address and Wi-Fi signal (`ip`, `wifi_rssi`) are included only when the broker connection has a **Username**, or **Share IP and Wi-Fi over MQTT** is on. On an anonymous broker everyone on the LAN could read them.

## Events

Events are not retained and use the topic `kiosk/<id>/event`. Each payload has a `type` and optional string fields. Values are length-capped and never carry secrets or full URLs.

| `type` | Fields | When |
|---|---|---|
| `command_rejected` | `command`, `reason` | A command was refused or invalid. |
| `exit_gesture` | | The exit corner was tapped five times. |
| `wrong_pin` | `attempts_left` or `locked_out` | A wrong admin PIN. |
| `page_error` | | The page failed to load. |
| `popup_shown` | `host` or `cameras` | A popup appeared. |
| `popup_dismissed` | `host` or `cameras`, `reason` | A popup closed. |
| `audio_error` | | Audio failed. |
| `device_moved` | | Tamper detection fired. |
| `kiosk_paused` | `until` | The lock was paused. `until` is an ISO time or `indefinite`. |
| `kiosk_resumed` | `reason` | `timeout`, `user`, `remote` or `idle`. |
| `wake_word_heard` | | The wake word was heard (voice on). |

Example refusal:

```json
{"type": "command_rejected", "command": "reboot", "reason": "Remote unlock is turned off on this tablet"}
```

## Commands

Publish to `kiosk/<id>/cmd/<command>`. The payload is either a plain value that fills the command's main argument, or a JSON object. Optional arguments need JSON. Payloads are limited to 4096 bytes. Invalid or refused commands produce a `command_rejected` event and nothing else, so subscribe to `event` while testing.

| Command | Plain payload | JSON payload |
|---|---|---|
| `reload` | empty | |
| `load_url` | `https://example.com/page` | `{"url": "..."}` |
| `screen` | `on` / `off` | `{"state": "on"}` |
| `brightness` | `128` | `{"level": 128}` |
| `screensaver` | `on` / `off` | `{"state": "off"}` |
| `dashboard` | `Energy` | `{"name": "Energy"}` |
| `tts` | `Hello` | `{"text": "Hello", "volume": 60}` |
| `play` | audio URL | `{"url": "...", "volume": 60}` |
| `stop_audio` | empty | |
| `volume` | `40` | `{"level": 40}` |
| `toast` | `Hello` | `{"text": "Hello", "duration": 8}` |
| `popup` | URL or `camera.<name>` | `{"url": "...", "timeout": 30, "title": "..."}` or `{"camera": ["camera.a", "camera.b"], "timeout": 30, "title": "..."}` |
| `dismiss_popup` | empty | |
| `snapshot` | empty | |
| `microphone` | `on` / `off` | `{"state": "off"}` |
| `kiosk_lock` | `on` / `off` | `{"state": "on"}` |
| `kiosk_resume` | empty | |
| `kiosk_pause` | `30` | `{"minutes": 30}` |
| `launch_app` | package name, or empty | `{"package": "com.example.app"}` or `{}` |
| `exit_app_mode` | empty | |
| `restart_app` | empty | |
| `clear_cache` | empty | |
| `reboot` | empty | |

Argument limits and details are the same as for REST, so see the [command table](rest-api.md#commands). `on` also accepts `true` and `1`, and `off` accepts `false` and `0`. Command names are case-insensitive.

### Policy

Every command passes one central policy, whatever its source. Over MQTT:

- **Benign** commands (everything that only changes what is shown or heard) always run.
- **Sensitive** commands (`kiosk_pause`, `kiosk_lock off`, `launch_app`, `exit_app_mode`, `restart_app`, `clear_cache`, `reboot`) are refused with `command_rejected` unless **Allow remote unlock & app control** is on, on the tablet.
- `reboot` also needs full lock (Device Owner), whatever the switch says.

The **Kiosk lock**, **Restart app** and **Relaunch app** entities are not even created in Home Assistant while remote unlock is off. See [Security](security.md#remote-control-policy).

## Discovery

With **Home Assistant discovery** on, Kiosky publishes a retained config for every entity at `homeassistant/<component>/<id>/<object>/config`. All entities share one device block with the identifier `kiosky_<id>`, the device name, model, manufacturer, app version and a link to the admin page. The full entity list is in [Home Assistant integration](home-assistant.md#entities).

When you rename the device, Kiosky clears the old retained availability and state. Discovery configs for entities that no longer apply are cleared, too, for example when you switch off remote unlock.

## Quick test

With the Mosquitto command-line tools:

```bash
# watch everything the tablet publishes
mosquitto_sub -h BROKER -u USER -P PASSWORD -t 'kiosk/hallway_tablet/#' -v

# show a toast
mosquitto_pub -h BROKER -u USER -P PASSWORD -t 'kiosk/hallway_tablet/cmd/toast' -m '{"text":"Hello","duration":5}'
```
