Program Option Discovery Caching#
Overview#
Program option discovery is the process by which the plugin learns what configurable options (temperature, spin speed, etc.) each appliance program supports. This information is not directly available from the Home Connect API without either passively observing a program's selected state, or actively selecting it first. The plugin implements a layered strategy β passive reading, active selection, and persistent caching β to minimise both API calls and appliance disruption.
The entire mechanism lives in the HasPrograms mixin (src/has-programs.ts), which is composed into every appliance type that supports programs.
Discovery Flow#
Initialisation triggers initPrograms(), which restores any previously cached program details from persistent storage before calling refreshPrograms().
Passive Reading#
updateProgramsWithoutSelecting() calls device.getAvailableProgram(key) via getCached() for each available program that has not yet had its options recorded. This works only when the program is already selected on the appliance β it will succeed for the currently selected program and may succeed for others where the API still returns details.
Active Reading (Select-First)#
updateProgramsSelectFirst() is used when the passive step leaves programs without options. It requires the Control scope and checks that :
- The appliance is in
InactiveorReadyoperation state (no active program). LocalControlActiveis not set.RemoteControlActiveis not explicitlyfalse.
For each qualifying program, selectAndGetAvailableProgram() powers the appliance on if needed, selects the target program, then calls getAvailableProgram() which wraps device.getAvailableProgram() with pre/post requireProgramReady() guards. After all programs are read, the original selected program and power state are restored on a best-effort basis.
The autoSelectingPrograms flag suppresses the BSH.Common.Root.SelectedProgram event handler during this phase to avoid re-entrant discovery.
Reactive Updates#
When the user manually selects a program on the appliance, updateSelectedProgram() fires via the BSH.Common.Root.SelectedProgram event. This opportunistically reads and caches the options for that program, marking it as selected: true.
Caching Layers#
In-flight Deduplication + Persistent Cache (getCached)#
All API calls for static appliance data go through getCached() in src/appliance-generic.ts. This method:
- Prevents duplicate concurrent calls for the same key.
- Delegates to
doCachedOperation(), which checksPersistCache.getWithExpiry()before making an API call. - Falls back to an expired cache entry if the API call fails .
Cache keys for program option reads are:
| Purpose | getCached key |
|---|---|
| Full program list | programs |
| Passive option read | program <ProgramKey> |
| Active (select-first) option read | program select <ProgramKey> |
Persistent Storage (PersistCache)#
PersistCache (src/persist-cache.ts) backs the cache with node-persist (localStorage-style). Cache entries expire after 24 hours and are also invalidated if:
- The stored entry was written by a different plugin version (semver major/minor mismatch) .
- The
preferredfield (reflecting current configuration) no longer matches .
The consolidated program details object (the programs array including discovered options) is separately persisted under the key Program details by savePrograms() after each discovery cycle, so it survives restarts without re-querying.
Program-Level selected Flag#
Within the in-memory programs array, each entry tracks a selected boolean . updateCachedProgram() enforces that a selected: true entry (read via active selection) cannot be overwritten by a passive read, protecting higher-quality data.
Failure Modes#
| Symptom | Cause |
|---|---|
| Program shows "missing options" | Program was never in an available state during any discovery cycle |
| Program shows "could not be selected" | Active reading was blocked (local control, remote control disabled, active program, no Control scope) |
| Options appear stale or wrong | Passive read succeeded but program was not actually selected; selected flag not set |
| Discovery silently skipped | Appliance was not in Inactive/Ready state when refreshPrograms() ran |
| Options disappear after plugin update | Cache invalidated by plugin version change; re-discovery required |
The missingOptionsHelp() method logs actionable recovery steps when any programs are missing options or could not be selected. Key guidance: ensure the appliance is idle with remote control enabled, then manually select each affected program on the appliance. The plugin will detect the selection event and opportunistically read the options. Alternatively, trigger re-discovery by restarting Homebridge or invoking the HomeKit Identify routine .
Some programs are legitimately never available β for example, Sabbath programs require a specific appliance setting before they appear .
Re-triggering Discovery#
- Restart Homebridge β
refreshPrograms(false)runs on every initialisation. - HomeKit Identify β calls
refreshPrograms(true)(active mode, re-reads all programs regardless of theselectedflag). - Manual program selection β triggers
updateSelectedProgram()reactively via theBSH.Common.Root.SelectedProgramevent.
Key Source Files#
| File | Role |
|---|---|
src/has-programs.ts | Discovery orchestration, caching logic, option validation |
src/appliance-generic.ts | getCached / doCachedOperation implementation |
src/persist-cache.ts | Persistent cache with TTL and version invalidation |
src/homeconnect-device.ts | getAllPrograms, getAvailablePrograms, getAvailableProgram API wrappers |