# Plugins ## Plugins (branch `plugins`, 2026-11-06) Status lines can show readings from Pebble plugins (2026-11-06: one dropdown, include favourites, build now even though store publishing of plugin consumers isn't open yet). Guide: `developer.repebble.com/guides/plugins/preview/`; reference consumers are Core's `complication-face` and `plugin-test` in `github.com/coredevices/pebble-apps`. Kept independent of the workspace's `homeassistant/` plugin: test against built-in sources. - **Needs SDK 4.44: `pebble build --sdk 4.44`** (4.33.1 stays the workspace's active SDK). Only 4.44's waf writes `manifestVersion: 2` and `usesPermissions` into `appinfo.json`; without them the phone treats the app as legacy, `enumeratePlugins()` returns nothing and every subscription fails `PERMISSION_DENIED`. 4.44 declares SDK rev `0x6e` (5.110), so the **minimum firmware is v4.44.3** (from pebbleOS tags, 2026-11-06: v4.43.0–v4.44.2 are `0x6d`, v4.44.3/4 `0x6e`, v4.45.0 `0x6f`; 2.3 needed v4.32.0). The rise was a deliberate call, 2026-11-06. - **Phones without plugins** (`plugins.state()`): an app before 1.16 has no plugin functions (`absent`); a current app with plugins switched off keeps the functions, but `enumeratePlugins()` lists nothing (not even the built-ins) and every subscription answers `PLUGIN_UNAVAILABLE` (`off`; a test phone, 2026-11-06). Either way no `Plugin reading` and no favourites are offered, and a line already set to one reads `update Pebble app` / `turn on plugins`. The state is read when the phone JS starts, so switching plugins back on shows at the next face relaunch. The settings page then shows a red-outlined note above the credit line (`pluginhelp` text item, filled from `userData.pluginHelp` by the `customFn`): `> plugins are off` with where to switch them on, or `> plugins need a newer Pebble app`. Only while a line is set to Plugin reading or a favourite; it follows the dropdowns live. Chromium-checked with the note injected (the emulator always has plugins on). Permissions are only enforced when plugins are enabled (guide), so weather behaves as before for them. - **Store status (2026-11-06)**: the guide (app 1.17.0.1) still says "Don't ship anything that uses/offers plugins to the app store yet" and calls the API unstable; Core's own demo consumers are in the store, third-party plugin publishing is invite-only. pebble-tool's publish has no gate for consumers. - **Manifest v2 means declaring what the JS uses**: `Location` (weather's geolocation, and weather sources), `Internet` for `api.open-meteo.com`, plus `Calendar` and `ReadMusic` so the calendar and music sources can be offered. Add any new host here or the phone blocks it. - **The picker offers only sources we have permission for.** `enumeratePlugins()` reports each source's `callerPermissions`; `plugins.js` reads `usesPermissions` from package.json and skips any source needing more. The emulator only *logs* missing grants (pypkjs `_log_grants`) and serves the data anyway, so without this filter Watch Settings (`ReadWatchSettings`) and Notifications (`Notifications`) showed up and would have read `needs permission` on a phone. To offer one, declare its permission. Each source is capped at 60 things. - **First run after install**: weather failed twice (`Location permission not granted`, then `HTTP 403` from the phone) until the phone granted the declared permissions, then recovered via the backoff a few minutes later (the test watch, 2026-11-06). Expected, per the guide: requests fail until the permission is answered. - **Settings** (two-step picker, 2026-11-06, after build 20's flat list was "crazy overcrowded"): each line's dropdown gets `Pebble favourite 1–3` (StatusField 18–20: the readings ranked in the Pebble app, `preferences/complications`) and, when any plugin has readings, `Plugin reading` (17). A line set to Plugin reading shows two more selects under it: - **from**: one entry per *thing* (a source instance), in optgroups by plugin (`Weather · location` only when a plugin has several sources). Named things by name (`London`); **positional** sources (every `instanceId` equals its index: calendar events, weather hours) are slots, so `event 1 (Design review)`; an unnamed one among several is `hour 3`; the only one is its item (`phone state`). Page-only, no messageKey. - **shows**: that thing's text-drawable properties (shortText, numericValue, boolean, timestamp; not `name`) with live values, `high: 14°C`. messageKey `PluginPick1–3`. `index.js` runs Clay with `autoHandleEvents: false`: on open, `plugins.catalogue()` subscribes to every source for up to 4 s and returns `{groups, readings}`; the options go into the config and `readings` into `meta.userData`, and the `customFn` shows/hides the pair and refills "shows" from `userData.readings` when "from" changes (raw DOM on `pick.$manipulatorTarget[0]`, then `trigger('change')` to update Clay's value label). A picked reading whose plugin is gone stays as `Unavailable / plugin reading`, or `Plugins off / plugin reading › turn on plugins` (`Plugins unsupported … › update Pebble app` on an old app), matching what the watch shows. Checked in headless Chromium: cascade, hiding, save. - **Not offered**: `preferences` and `agent` categories, and items named `configured` (Core's demo plugins use them as a set-up flag), besides anything we lack permission for (below). - **A picked reading** is stored in Clay's settings as `PluginPickN` = `p|plugin|category|item|instanceId|property` (parts URI-escaped). `webviewclosed` deletes the `PluginPick` keys before sending, so the watch only gets FIELD_PLUGIN (17). Build 20 stored the key in `FieldN` itself; opening the page migrates it, and `plugins.js` reads either. - **The phone formats, the watch draws** (`src/pkjs/plugins.js`): `name value` in 17 characters, label shortened first, at a word break when that leaves ≥3 characters (`Design 22:00`); an unnamed instance among several is `item N` (`hour 3 20°C`); only ASCII, `°`, `·` survive (accents dropped). Instance missing → `not found`, `PERMISSION_DENIED` → `needs permission`, `PLUGIN_UNAVAILABLE` → `plugin missing`, other errors keep the last text. Sent as `PluginLine1–3`. - **Rate limit** (every AppMessage holds BLE fast for 10 s): 2 s debounce, then only on change, at most every 5 min (the latest value wins), plus an hourly resend of an unchanged line. The last delivered texts live in `localStorage` (`pluginSent`), so a face relaunch sends nothing. Saving settings sends at once. Node-simulated with a fake clock, 2026-11-06. - **Plugin lines stay white** (2026-11-06): colour in the status area means something (steps vs goal, battery charge), and a plugin's value carries no such meaning. - **Watch side**: persist key **400** (`PluginLines`: per line, the text and when it arrived). `waiting for phone` until the first text, `no plugin data` after 2 h. A settings message that touches a plugin line clears its text. - **Emulator** (4.44 emery, 2026-11-06): built-ins offered are Phone, Calendar, Watch Settings, Music, Notifications, Weather. There's **no Preferences plugin** there, so favourites read `no favourite N` (which also caught a bug: lines waited on favourites before subscribing). Verified: catalogue, save, `Palo Alto 18°C` / `battery level 87%` on the face, no resend on relaunch, weather still fetched with permissions declared. Gabbro only built, not run. - **Wrist-only questions**: whether favourites fill in (no Preferences plugin in the emulator); whether the phone prompts for Calendar/ReadMusic at install or on first use; whether the Clay page loads normally for a manifest v2 app. The first wrist run showed the page loading and readings arriving.