|
| 1 | +# ADR-061 - Building model persistence over software reset and Homey app update |
| 2 | + |
| 3 | +**Project:** org.hhi.adlar3-heatpump-modbus |
| 4 | +**Status:** voorgesteld |
| 5 | +**Datum:** 2026-06-11 |
| 6 | +**Gerelateerd:** ADR-037 store-persistentie, ADR-056 building model hardening, ADR-057 learner safeguards, ADR-060 adaptive harmonisatie |
| 7 | +**Codegebied:** `lib/services/building-model-service.ts`, `lib/services/adaptive-control-service.ts`, `lib/services/service-coordinator.ts`, `drivers/intelligent-heatpump-modbus/device.ts` |
| 8 | + |
| 9 | +## Context |
| 10 | + |
| 11 | +Het gebouwmodel leert thermische parameters van de woning via `BuildingModelLearner`: |
| 12 | + |
| 13 | +- `C`: thermische massa |
| 14 | +- `UA`: warmteverlies |
| 15 | +- `g`: zonnewinst |
| 16 | +- `P_int`: interne warmtelast |
| 17 | +- `sampleCount`, `theta`, `P`, `lastMeasurement` en excitation counters als interne RLS-state |
| 18 | + |
| 19 | +Deze state is `engine-state` en hoort persistent te zijn over: |
| 20 | + |
| 21 | +- software reset van de Homey app |
| 22 | +- Homey reboot |
| 23 | +- app update/install |
| 24 | +- coordinator restart door gewijzigde Modbus/poll-settings |
| 25 | + |
| 26 | +De user-facing instellingen voor het gebouwmodel zijn Homey device settings: |
| 27 | + |
| 28 | +- `building_model_enabled` |
| 29 | +- `building_model_forgetting_factor` |
| 30 | +- `building_profile` |
| 31 | +- `enable_dynamic_pint` |
| 32 | +- `reset_building_model` |
| 33 | + |
| 34 | +Deze settings horen door Homey zelf persistent beheerd te worden. De geleerde modelstate hoort door de app in device store te worden beheerd onder `building_model_state`. |
| 35 | + |
| 36 | +## Vraagstelling |
| 37 | + |
| 38 | +De vraag is of de building modeler na software reset, Homey reboot, app update/install of coordinator restart zijn configuratie en geleerde state behoudt, en via welke persistentielaag dat gebeurt. |
| 39 | + |
| 40 | +Daarbij moeten twee soorten data strikt uit elkaar worden gehouden: |
| 41 | + |
| 42 | +- **Configuratie/settings**: Homey device settings uit `driver.settings.compose.json`, gelezen via `device.getSetting()`. |
| 43 | +- **Geleerde modelstate**: RLS-engine-state van `BuildingModelLearner`, opgeslagen in device store onder `building_model_state`. |
| 44 | + |
| 45 | +## Analyse |
| 46 | + |
| 47 | +Bij code-inspectie zijn twee concrete risico's zichtbaar voor de geleerde samples/state: |
| 48 | + |
| 49 | +1. `BuildingModelService.collectAndLearn()` persist `building_model_state` alleen elke 10 geaccepteerde samples. |
| 50 | + - Bij een restart/update vóór sample 10 is er nog geen state opgeslagen. |
| 51 | + - Na sample 10 kan nog steeds maximaal 9 samples, dus maximaal 45 minuten bij 5-minuten interval, verloren gaan. |
| 52 | + |
| 53 | +2. `ServiceCoordinator.destroy()` roept `this.adaptiveControl.destroy()` aan zonder `await`. |
| 54 | + - `AdaptiveControlService.destroy()` is async en wacht intern op `this.buildingModel.destroy()`. |
| 55 | + - `BuildingModelService.destroy()` doet de finale persist van `building_model_state`. |
| 56 | + - Zonder `await` kan app teardown doorgaan terwijl de finale persist nog niet afgerond is. |
| 57 | + |
| 58 | +Voor de settings zelf is in de huidige code geen app-eigen persistencepad nodig of aanwezig: Homey device settings zijn de authoritative source. De app leest deze waarden op bij service-constructie, reset en settings-wijzigingen. Zolang de setting-id's gelijk blijven en het device niet opnieuw gepaired wordt, horen deze settings door Homey bewaard te blijven over reboot en app update. |
| 59 | + |
| 60 | +## Besluit |
| 61 | + |
| 62 | +We behandelen building model persistence als een expliciet lifecycle-contract: |
| 63 | + |
| 64 | +1. **Elke geaccepteerde learning sample wordt direct persistent gemaakt.** |
| 65 | + - `learner.addMeasurement()` succesvol afgerond betekent: `building_model_state` moet naar device store. |
| 66 | + - UI-capabilities en diagnostics blijven rate-limited, bijvoorbeeld elke 10 samples. |
| 67 | + - Rationale: een 5-minuten sample is klein; betrouwbaarheid is belangrijker dan micro-optimalisatie van store writes. |
| 68 | + |
| 69 | +2. **Shutdown moet alle async persistence afwachten.** |
| 70 | + - `ServiceCoordinator.destroy()` moet `await this.adaptiveControl.destroy()`. |
| 71 | + - `AdaptiveControlService.destroy()` blijft verantwoordelijk voor het awaiten van `BuildingModelService.destroy()`. |
| 72 | + - Destroy-volgorde moet geen subservice vernietigen voordat diens persist klaar is. |
| 73 | + |
| 74 | +3. **Restore logging moet aantonen wat er is hersteld.** |
| 75 | + - Bij `BuildingModelService.initialize()` loggen: |
| 76 | + - store-key aanwezig/afwezig |
| 77 | + - sample count |
| 78 | + - confidence |
| 79 | + - modelparameters op hoofdlijnen |
| 80 | + - restore reject met validatiefout indien van toepassing |
| 81 | + - Dit maakt het verschil zichtbaar tussen: |
| 82 | + - geen state opgeslagen |
| 83 | + - state aanwezig maar corrupt/rejected |
| 84 | + - state correct hersteld |
| 85 | + |
| 86 | +4. **Settings blijven Homey-managed.** |
| 87 | + - `building_model_*` settings blijven Homey settings, niet dupliceren in store. |
| 88 | + - Deze ADR wijzigt geen settingsopslag en introduceert geen settings-restore vanuit store. |
| 89 | + - `reset_building_model` blijft een expliciete destructive action en mag niet impliciet door restart/update worden getriggerd. |
| 90 | + |
| 91 | +## Openstaande punten |
| 92 | + |
| 93 | +### 1. Persist-frequentie |
| 94 | + |
| 95 | +**Huidig gedrag:** persist alleen bij `sampleCount % 10 === 0` en bij destroy. |
| 96 | +**Risico:** alle samples sinds laatste checkpoint kwijt bij harde restart/update. |
| 97 | + |
| 98 | +**Behandeling:** |
| 99 | + |
| 100 | +- Na elke succesvolle `addMeasurement()` direct `await persistState()`. |
| 101 | +- `updateModelCapabilities()` en `updateDiagnosticsCapability()` blijven op 10-sample cadence. |
| 102 | +- Bij persist-fout: loggen, maar learning niet blokkeren. |
| 103 | + |
| 104 | +**Acceptatiecriterium:** |
| 105 | + |
| 106 | +- Na 1 geaccepteerde sample staat `building_model_state.sampleCount >= 1` in store. |
| 107 | +- Na 9 samples en een gesimuleerde restart wordt sampleCount 9 hersteld, niet 0. |
| 108 | + |
| 109 | +### 2. Async destroy-chain |
| 110 | + |
| 111 | +**Huidig gedrag:** `ServiceCoordinator.destroy()` roept `this.adaptiveControl.destroy()` zonder `await`. |
| 112 | +**Risico:** finale building-model persist wordt niet gegarandeerd afgerond. |
| 113 | + |
| 114 | +**Behandeling:** |
| 115 | + |
| 116 | +- Wijzig naar `await this.adaptiveControl.destroy()`. |
| 117 | +- Houd bestaande expliciete optimizer-state save intact. |
| 118 | +- Zorg dat errors in destroy worden gelogd zonder verdere cleanup volledig te blokkeren. |
| 119 | + |
| 120 | +**Acceptatiecriterium:** |
| 121 | + |
| 122 | +- Een test met mock-device-store toont dat `building_model_state` geschreven is voordat `ServiceCoordinator.destroy()` resolved. |
| 123 | + |
| 124 | +### 3. Restore-validatie |
| 125 | + |
| 126 | +**Huidig gedrag:** `BuildingModelLearner.restoreState()` reject corrupte theta/P-state defensief. |
| 127 | +**Risico:** correcte reject lijkt voor gebruiker op "alles kwijt" als de reden niet zichtbaar is. |
| 128 | + |
| 129 | +**Behandeling:** |
| 130 | + |
| 131 | +- Restorepad logt expliciet of state: |
| 132 | + - afwezig is |
| 133 | + - geldig hersteld is |
| 134 | + - rejected is, inclusief validatiefout |
| 135 | +- Diagnostics krijgen zo nodig een `lastRestoreStatus` of vergelijkbaar veld, als gewone logging onvoldoende blijkt. |
| 136 | + |
| 137 | +**Acceptatiecriterium:** |
| 138 | + |
| 139 | +- Bij corrupte persisted state is in logs/diagnostics zichtbaar waarom het model op defaults start. |
| 140 | + |
| 141 | +### 4. Homey settings versus learned state |
| 142 | + |
| 143 | +**Huidig gedrag:** settings staan in `driver.settings.compose.json`; learned state staat in store. |
| 144 | +**Risico:** verwarring tussen twee persistentielagen kan het probleem verkeerd laten behandelen. |
| 145 | + |
| 146 | +**Behandeling:** |
| 147 | + |
| 148 | +- Bij startup restore diagnostics mogen de actuele Homey settingswaarden mee gelogd worden voor context: |
| 149 | + - `building_model_enabled` |
| 150 | + - `building_model_forgetting_factor` |
| 151 | + - `building_profile` |
| 152 | + - `enable_dynamic_pint` |
| 153 | +- Niet automatisch settings naar store kopiëren. |
| 154 | +- Geen settings-migratie of settings-herstel toevoegen zolang setting-id's stabiel blijven en er geen reproduceerbaar Homey settingsverlies is vastgesteld. |
| 155 | + |
| 156 | +**Acceptatiecriterium:** |
| 157 | + |
| 158 | +- Na update leest de app de Homey settings via dezelfde setting-id's. |
| 159 | +- `building_model_state` wordt afzonderlijk hersteld uit device store. |
| 160 | +- Startup diagnostics maken zichtbaar welke settingswaarden zijn gelezen en welke learned state is hersteld. |
| 161 | + |
| 162 | +### 5. Reset-toggle veiligheid |
| 163 | + |
| 164 | +**Huidig gedrag:** `reset_building_model=true` triggert `BuildingModelService.reset()` en zet daarna de toggle terug naar false via deferred `setSettings()`. |
| 165 | +**Risico:** als de deferred reset van de toggle niet lukt, kan een latere settings-cyclus opnieuw resetten. |
| 166 | + |
| 167 | +**Behandeling:** |
| 168 | + |
| 169 | +- Log altijd of de toggle succesvol teruggezet is. |
| 170 | +- Overweeg een store-based one-shot guard met timestamp/request-id als herhaalde resets in praktijk voorkomen. |
| 171 | +- Geen wijziging tenzij reproduceerbaar, omdat extra guards complexiteit toevoegen. |
| 172 | + |
| 173 | +**Acceptatiecriterium:** |
| 174 | + |
| 175 | +- Een mislukte toggle-reset is zichtbaar in logs. |
| 176 | +- Er is geen impliciete reset bij normale app update zonder expliciete user action. |
| 177 | + |
| 178 | +## Voorgestelde implementatievolgorde |
| 179 | + |
| 180 | +1. Maak `BuildingModelService` crash-safe voor samples: |
| 181 | + - persist na elke geaccepteerde sample |
| 182 | + - behoud 10-sample cadence voor capabilities/diagnostics |
| 183 | + |
| 184 | +2. Maak shutdown await-safe: |
| 185 | + - `await this.adaptiveControl.destroy()` in `ServiceCoordinator.destroy()` |
| 186 | + |
| 187 | +3. Verbeter restore-observability: |
| 188 | + - log sample count/confidence bij restore |
| 189 | + - log afwezig/rejected/geldig expliciet |
| 190 | + |
| 191 | +4. Voeg tests toe: |
| 192 | + - unit/mock-test voor persist na sample |
| 193 | + - unit/mock-test voor awaited destroy-chain |
| 194 | + - simulatie of service-test voor restart na minder dan 10 samples |
| 195 | + |
| 196 | +5. Pas `docs/Dev support/store-persistentie-matrix.md` aan: |
| 197 | + - markeer `building_model_state` pas weer correct wanneer de fix en tests aanwezig zijn |
| 198 | + - noteer persist-frequentie als "per accepted sample + final destroy" |
| 199 | + |
| 200 | +## Teststrategie |
| 201 | + |
| 202 | +Minimale testdekking voor deze ADR: |
| 203 | + |
| 204 | +1. **Service persistence test** |
| 205 | + - Mock `Homey.Device` met `getStoreValue` en `setStoreValue`. |
| 206 | + - Injecteer geldige indoor/outdoor/power/COP data. |
| 207 | + - Forceer een `collectAndLearn()`-cyclus of exposeer een testbare helper. |
| 208 | + - Assert: `setStoreValue('building_model_state', ...)` is aangeroepen na sample 1. |
| 209 | + |
| 210 | +2. **Restart restore test** |
| 211 | + - Maak learner/service, voeg 1-9 samples toe, persist store. |
| 212 | + - Maak nieuwe service met dezelfde mock-store. |
| 213 | + - Assert: restored `sampleCount` is gelijk aan opgeslagen count. |
| 214 | + |
| 215 | +3. **Destroy-chain test** |
| 216 | + - Mock `AdaptiveControlService.destroy()` als async promise. |
| 217 | + - Assert: `ServiceCoordinator.destroy()` resolved pas na die promise. |
| 218 | + |
| 219 | +4. **Manual update checklist** |
| 220 | + - Voor release: app installeren over bestaande versie met actief building model. |
| 221 | + - Voor update: noteer `building_model_state.sampleCount` uit restore diagnostics. |
| 222 | + - Na update: sample count en settings blijven behouden. |
| 223 | + |
| 224 | +## Consequenties |
| 225 | + |
| 226 | +Positief: |
| 227 | + |
| 228 | +- Geen verlies van vroege learning na update/restart. |
| 229 | +- Hooguit 0 samples verlies na nette shutdown; bij harde stroomuitval maximaal de sample die op dat moment nog niet geschreven was. |
| 230 | +- Betere diagnose wanneer state ontbreekt of rejected wordt. |
| 231 | + |
| 232 | +Negatief: |
| 233 | + |
| 234 | +- Meer store writes: maximaal een write per 5 minuten per device bij actief leren. |
| 235 | +- Iets meer logging bij startup/restore. |
| 236 | + |
| 237 | +Acceptabel, omdat learning-state schaars en waardevol is en de write-frequentie laag blijft. |
| 238 | + |
| 239 | +## Niet doen |
| 240 | + |
| 241 | +- Learned state niet naar Homey settings kopiëren. |
| 242 | +- Settings niet dupliceren in store zonder bewijs dat Homey settings zelf niet persistent zijn. |
| 243 | +- `reset_building_model` niet automatisch activeren bij corrupt state; corrupt state moet zichtbaar worden en alleen expliciet resetten. |
| 244 | +- Geen directe edit aan gegenereerde `app.json`; settings blijven via compose. |
0 commit comments