diff --git a/README.md b/README.md
index 9fae8532..4f11209a 100644
--- a/README.md
+++ b/README.md
@@ -145,6 +145,12 @@ curl -X POST http://your-pi-ip:5000/api/v3/plugins/install \
| [MQTT Notifications](./plugins/mqtt-notifications/) | HomeAssistant notifications via MQTT | |
| [On Air Light](./plugins/on-air/) | Broadcast ON AIR tally light, triggered remotely via MQTT / Home Assistant | |
+### Productivity (1)
+
+| Plugin | Description | Preview |
+|--------|-------------|---------|
+| [Pomodoro Timer](./plugins/pomodoro-timer/) | Focus/break timer with MQTT control and Home Assistant discovery |
|
+
### Custom (3)
| Plugin | Description | Preview |
diff --git a/plugins.json b/plugins.json
index f900d57c..7fddaac5 100644
--- a/plugins.json
+++ b/plugins.json
@@ -1165,10 +1165,10 @@
"plugin_path": "plugins/pomodoro-timer",
"stars": 0,
"downloads": 0,
- "last_updated": "2026-07-29",
+ "last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
- "latest_version": "1.3.4",
+ "latest_version": "1.3.5",
"icon": "fa-hourglass-half"
}
]
diff --git a/plugins/pomodoro-timer/README.md b/plugins/pomodoro-timer/README.md
index 9d5a0a14..5566e3f2 100644
--- a/plugins/pomodoro-timer/README.md
+++ b/plugins/pomodoro-timer/README.md
@@ -153,62 +153,89 @@ durations you configured.
## Plugin Configuration
+Settings are edited in the plugin's tab in the LEDMatrix web UI. The **Field**
+column is the label you see there; the **Key** is the same setting as it appears
+in `config/config.json` under `pomodoro-timer`, which is what you need if you
+edit the file directly or read it back in a log line. The full schema is
+[`config_schema.json`](config_schema.json).
+
### Timer
-| Field | Default | Description |
-|---|---|---|
-| **Work Session (minutes)** | `25` | Length of a focus session. Also settable live from Home Assistant. |
-| **Short Break (minutes)** | `5` | Break after each work session. |
-| **Long Break (minutes)** | `15` | Break after a full set of work sessions. |
-| **Sessions Before Long Break** | `4` | How many work sessions make a set — this is the number of dots on screen. |
-| **Auto-Start Breaks** | `true` | Roll straight into the break when work ends. Off makes the break wait for a Start/Resume. |
-| **Auto-Start Next Work Session** | `false` | Roll straight back into work when a break ends. Off by default so you decide when to go again. |
-| **Start Timer When Plugin Is Enabled** | `false` | Begin a work session as soon as the plugin loads. Handy without MQTT. |
+| Field | Key | Default | Description |
+|---|---|---|---|
+| *(header toggle)* | `enabled` | `true` | Master switch. It is the toggle in the tab's header rather than a field in the form. Off removes the timer from the rotation entirely. |
+| **Work Session (minutes)** | `work_minutes` | `25` | Length of a focus session (1–180). Also settable live from Home Assistant. |
+| **Short Break (minutes)** | `short_break_minutes` | `5` | Break after each work session (1–60). |
+| **Long Break (minutes)** | `long_break_minutes` | `15` | Break after a full set of work sessions (1–120). |
+| **Sessions Before Long Break** | `sessions_before_long_break` | `4` | How many work sessions make a set (1–12) — this is the number of dots on screen. |
+| **Auto-Start Breaks** | `auto_start_breaks` | `true` | Roll straight into the break when work ends. Off makes the break wait for a Start/Resume. |
+| **Auto-Start Next Work Session** | `auto_start_work` | `false` | Roll straight back into work when a break ends. Off by default so you decide when to go again. |
+| **Start Timer When Plugin Is Enabled** | `auto_start_on_enable` | `false` | Begin a work session as soon as the plugin loads. Handy without MQTT. |
### MQTT
-| Field | Default | Description |
-|---|---|---|
-| **Enable MQTT Control** | `true` | Connect to a broker so the timer can be driven remotely. |
-| **Broker Address** | `localhost` | IP or hostname of your MQTT broker. |
-| **Broker Port** | `1883` | Use 8883 for TLS. |
-| **Username / Password** | *(blank)* | Leave blank for an anonymous broker. |
-| **Command Topic** | `ledmatrix/pomodoro/set` | Everything the plugin publishes is derived from this topic's base. |
-| **State Topic** | `ledmatrix/pomodoro/state` | `ON` while a session is active, `OFF` when idle. |
-| **Enable Home Assistant Auto-Discovery** | `true` | Announce the device to HA over MQTT. |
-| **HA Discovery Prefix** | `homeassistant` | Only change this if you changed it in HA. |
-| **Device Name in Home Assistant** | `LED Matrix — Pomodoro` | How the device is labelled in HA. |
-| **State Publish Interval (seconds)** | `1` | How often the countdown is published while running. Raise it to cut broker traffic — state *changes* are always published immediately. |
+| Field | Key | Default | Description |
+|---|---|---|---|
+| **Enable MQTT Control** | `mqtt_enabled` | `true` | Connect to a broker so the timer can be driven remotely. |
+| **Broker Address** | `mqtt_host` | `localhost` | IP or hostname of your MQTT broker. |
+| **Broker Port** | `mqtt_port` | `1883` | Use 8883 for TLS. |
+| **Username / Password** | `mqtt_username`
`mqtt_password` | *(blank)* | Leave blank for an anonymous broker. The password is marked secret, so the web UI masks it. |
+| **Command Topic** | `command_topic` | `ledmatrix/pomodoro/set` | Everything the plugin publishes is derived from this topic's base. |
+| **State Topic** | `state_topic` | `ledmatrix/pomodoro/state` | `ON` while a session is active, `OFF` when idle. |
+| **Enable Home Assistant Auto-Discovery** | `ha_discovery` | `true` | Announce the device to HA over MQTT. |
+| **HA Discovery Prefix** | `discovery_prefix` | `homeassistant` | Only change this if you changed it in HA. |
+| **Device Name in Home Assistant** | `device_name` | `LED Matrix — Pomodoro` | How the device is labelled in HA. |
+| **State Publish Interval (seconds)** | `publish_interval_seconds` | `1` | How often the countdown is published while running (1–60). Raise it to cut broker traffic — state *changes* are always published immediately. |
### Appearance
-| Field | Default | Description |
-|---|---|---|
-| **Countdown Color** | `phase` | `phase` colors the countdown by what's running; `fixed` always uses the Countdown Text Color. |
-| **Colour Theme** | `classic` | `classic` uses the individual phase colours below. `calm` overrides them with a softer palette — warm terracotta, sage, soft indigo. |
-| **Work / Short Break / Long Break Color** | red / green / blue | Phase colors. |
-| **Idle / Paused Color** | grey / amber | Used when nothing is running and when paused. |
-| **How Paused Looks** | `amber` | `amber` switches the countdown to the Paused Colour. `desaturate` keeps the phase's own hue but drains it, so a held timer reads as halted without changing which phase you're in. |
-| **Countdown Text Color** | white | Only used when Countdown Color is `fixed`. |
-| **Background Color** | black | Panel background. |
-| **Countdown Digits** | `seven_segment` | `seven_segment` draws clock-radio style segments sized to the panel. `pixel` uses the display's pixel font. |
-| **Show Unlit Segments** | `false` | Faintly light the unlit segments, like a real LED clock. Authentic, but it costs legibility — every digit gains a faint `8` behind it. Ignored when the stroke is only one pixel wide, where the effect would just be noise. |
-| **Burndown Indicator** | `perimeter` | `perimeter` drains a ring around the edge of the panel; `bar` empties a bar along the bottom; `segments` puts out a row of blocks one at a time; `none` hides it. |
-| **Show Phase Label** | `true` | The phase name above the countdown. |
-| **Show Session Dots** | `true` | One pip per session in the set. Completed are solid, upcoming are hollow, and the one you're in is picked out. |
-| **Pulse the Current Session Dot** | `true` | Slowly blink the pip for the session you're in, so the row reads as "two done, on the third" rather than just a count. |
-| **Work / Short Break / Long Break / Idle / Paused Label** | `FOCUS` / `BREAK` / `LONG BREAK` / `POMODORO` / `PAUSED` | The on-screen text for each state. Blank the Paused Label to keep showing the phase name while paused. |
-| **Font** | *(blank)* | Path to a TTF relative to the LEDMatrix root, e.g. `assets/fonts/PressStart2P-Regular.ttf`. Blank uses the display's default font. |
-| **Font Size (px)** | `0` | Fix the countdown height in pixels. `0` sizes it automatically to the panel. |
+| Field | Key | Default | Description |
+|---|---|---|---|
+| **Countdown Color** | `color_mode` | `phase` | `phase` colors the countdown by what's running; `fixed` always uses the Countdown Text Color. |
+| **Colour Theme** | `color_theme` | `classic` | `classic` uses the individual phase colours below. `calm` overrides them with a softer palette — warm terracotta, sage, soft indigo. |
+| **Work / Short Break / Long Break Color** | `work_color`
`short_break_color`
`long_break_color` | red / green / blue | Phase colors, each `[r, g, b]`. |
+| **Idle / Paused Color** | `idle_color`
`paused_color` | grey / amber | Used when nothing is running and when paused. |
+| **How Paused Looks** | `paused_style` | `amber` | `amber` switches the countdown to the Paused Colour. `desaturate` keeps the phase's own hue but drains it, so a held timer reads as halted without changing which phase you're in. |
+| **Countdown Text Color** | `time_color` | white | Only used when Countdown Color is `fixed`. |
+| **Background Color** | `background_color` | black | Panel background. |
+| **Countdown Digits** | `digit_style` | `seven_segment` | `seven_segment` draws clock-radio style segments sized to the panel. `pixel` uses the display's pixel font. |
+| **Show Unlit Segments** | `show_ghost_segments` | `false` | Faintly light the unlit segments, like a real LED clock. Authentic, but it costs legibility — every digit gains a faint `8` behind it. Ignored when the stroke is only one pixel wide, where the effect would just be noise. |
+| **Burndown Indicator** | `progress_style` | `perimeter` | `perimeter` drains a ring around the edge of the panel; `bar` empties a bar along the bottom; `segments` puts out a row of blocks one at a time; `none` hides it. |
+| **Show Phase Label** | `show_phase_label` | `true` | The phase name above the countdown. |
+| **Show Session Dots** | `show_session_dots` | `true` | One pip per session in the set. Completed are solid, upcoming are hollow, and the one you're in is picked out. |
+| **Pulse the Current Session Dot** | `pulse_active_pip` | `true` | Slowly blink the pip for the session you're in, so the row reads as "two done, on the third" rather than just a count. |
+| **Work / Short Break / Long Break / Idle / Paused Label** | `work_label`
`short_break_label`
`long_break_label`
`idle_label`
`paused_label` | `FOCUS` / `BREAK` / `LONG BREAK` / `POMODORO` / `PAUSED` | The on-screen text for each state, up to 24 characters. Blank the Paused Label to keep showing the phase name while paused. |
+| **Font** | `font_path` | *(blank)* | Path to a TTF relative to the LEDMatrix root, e.g. `assets/fonts/PressStart2P-Regular.ttf`. Blank uses the display's default font. |
+| **Font Size (px)** | `font_size` | `0` | Fix the countdown height in pixels. `0` sizes it automatically to the panel. |
### Behavior
-| Field | Default | Description |
-|---|---|---|
-| **Hold the Display While Running** | `true` | Take over the matrix while a session is active instead of rotating. Off lets the timer take a normal turn in the rotation. |
-| **Phase-Change Alert (seconds)** | `8` | How long the timer grabs the display when a phase ends. `0` disables it. |
-| **Flash on Phase Change** | `true` | Flash the panel during that alert. |
-| **Display Duration (seconds)** | `10` | Time on screen per rotation cycle when it isn't holding the display. |
+| Field | Key | Default | Description |
+|---|---|---|---|
+| **Hold the Display While Running** | `pin_while_running` | `true` | Take over the matrix while a session is active instead of rotating. Off lets the timer take a normal turn in the rotation. |
+| **Phase-Change Alert (seconds)** | `alert_seconds` | `8` | How long the timer grabs the display when a phase ends. `0` disables it. |
+| **Flash on Phase Change** | `alert_flash` | `true` | Flash the panel during that alert. |
+| **Display Duration (seconds)** | `display_duration` | `10` | Time on screen per rotation cycle when it isn't holding the display. |
+
+Fine-tuning settings are collected into a collapsed **Advanced Settings**
+section below the main fields in the web UI. They are listed above alongside
+everything else.
+
+### A minimal configuration
+
+```json
+{
+ "pomodoro-timer": {
+ "enabled": true,
+ "work_minutes": 25,
+ "short_break_minutes": 5,
+ "long_break_minutes": 15,
+ "sessions_before_long_break": 4,
+ "mqtt_enabled": true,
+ "mqtt_host": "homeassistant.local"
+ }
+}
+```
---
diff --git a/plugins/pomodoro-timer/manifest.json b/plugins/pomodoro-timer/manifest.json
index 1a58c1ec..3db444b7 100644
--- a/plugins/pomodoro-timer/manifest.json
+++ b/plugins/pomodoro-timer/manifest.json
@@ -1,7 +1,7 @@
{
"id": "pomodoro-timer",
"name": "Pomodoro Timer",
- "version": "1.3.4",
+ "version": "1.3.5",
"author": "ChuckBuilds",
"description": "A configurable Pomodoro focus/break timer for your matrix. Set the work and break lengths, then start, pause, skip, or reset it over MQTT \u2014 with Home Assistant auto-discovery so the whole timer shows up as a device with no YAML.",
"entry_point": "manager.py",
@@ -22,6 +22,12 @@
">=2.0.0"
],
"versions": [
+ {
+ "released": "2026-09-02",
+ "version": "1.3.5",
+ "notes": "Documentation. Every setting in the README now carries its config.json key alongside the web-UI label, so the tables can be used when editing the file directly rather than only when clicking through the form; all 46 schema properties are covered. Corrects two claims: 'enabled' is the tab's header toggle rather than a form field, and the advanced settings appear in a collapsed 'Advanced Settings' section. No functional change.",
+ "ledmatrix_min_version": "2.0.0"
+ },
{
"released": "2026-08-15",
"version": "1.3.4",
@@ -88,7 +94,7 @@
"license": "GPL-3.0",
"homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/pomodoro-timer",
"config_schema": "config_schema.json",
- "last_updated": "2026-07-29",
+ "last_updated": "2026-09-02",
"stars": 0,
"downloads": 0,
"verified": true,
diff --git a/scripts/update_readme_previews.py b/scripts/update_readme_previews.py
index 2893b2b9..c30ed356 100644
--- a/scripts/update_readme_previews.py
+++ b/scripts/update_readme_previews.py
@@ -4,7 +4,8 @@
The "Available Plugins" tables in ``README.md`` carry a thumbnail of each
plugin's hero screenshot. This script rewrites that column from what is
actually on disk: a plugin gets a thumbnail once
-``docs/assets//hero.png`` exists, and an empty cell until then.
+``docs/assets//hero.png`` (or ``plugins//assets/hero.png``,
+for plugins that ship their own renderer) exists, and an empty cell until then.
Run it after adding a plugin's README screenshots::
@@ -40,14 +41,30 @@
SEPARATOR = "|--------|-------------|---------|"
+def hero_path(plugin_id: str) -> str | None:
+ """Where this plugin's hero image lives, as a repo-relative URL.
+
+ Most plugins render theirs into ``docs/assets//``. A few carry their own
+ renderer and keep the image beside the plugin, so look there too rather than
+ leaving those rows blank.
+ """
+ shared = ASSETS_ROOT / plugin_id / "hero.png"
+ if shared.is_file():
+ return f"./docs/assets/{plugin_id}/hero.png"
+ local = REPO_ROOT / "plugins" / plugin_id / "assets" / "hero.png"
+ if local.is_file():
+ return f"./plugins/{plugin_id}/assets/hero.png"
+ return None
+
+
def preview_cell(plugin_id: str) -> str:
"""The Preview cell for one plugin: a thumbnail, or empty if none yet."""
- hero = ASSETS_ROOT / plugin_id / "hero.png"
- if not hero.is_file():
+ hero = hero_path(plugin_id)
+ if hero is None:
return " "
return (
f' '
- f'
'
)