iobroker.laundrylens 0.4.37 → 0.4.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -132,6 +132,17 @@ This also works inside the conditional `[...]` blocks: `[🌡️ Outside: {state
132
132
 
133
133
  ### **WORK IN PROGRESS**
134
134
 
135
+ ### 0.4.39 (2026-10-06)
136
+ - Fix: reported live - `elapsedTime` stayed frozen (often at a stale value left over from a previous cycle) for the entire "detecting..." period of a new cycle, only starting to update once a program was actually confirmed (observed: stuck at 11342 while only ~15 minutes into a new washer cycle; for a dryer, jumped from frozen to 1096 the moment the program was recognized). Root cause: `elapsedTime` is just `Date.now()` minus the cycle's start time, with no real dependency on program detection - but the `onTimeUpdate` callback that wrote it (via `_onTime()`) returned early, skipping the write entirely, whenever `WashDataManager._updateTimeEstimate()` reported no program/bestCandidate yet
137
+ - Fixed by extracting the computation into a shared `_updateElapsedTime()` helper, now called unconditionally at the top of `onTimeUpdate` - before its "no program detected" early return - as well as from `_onTime()`'s normal path, so it updates from the moment a cycle starts running regardless of detection status
138
+ - Covered by a new regression test (`tests/test_elapsed_time_before_detection.js`)
139
+
140
+ ### 0.4.38 (2026-10-04)
141
+ - New: the Lernkontrolle (learning review/feedback) flow can now be driven from a VIS dashboard, not just the admin tab - previously only reachable via sendTo commands, invisible to VIS, which can only read/write data points
142
+ - New read-only data points: `pendingFeedbackCount`, `pendingFeedback` (JSON array of all unconfirmed cycles, oldest first), and `feedbackCycleId`/`feedbackProgram`/`feedbackDuration`/`feedbackEnergy`/`feedbackConfidence` (convenience fields for the oldest pending cycle, so a simple VIS text widget doesn't need to parse JSON)
143
+ - New writable actions, each acting on the oldest pending cycle: `feedbackConfirm` (button - same as the admin tab's "Correct – confirm"), `feedbackCorrectProgram` (dropdown of program names, kept in sync the same way `programOverride`'s own dropdown is - writing a name corrects and confirms, same as the admin tab's "Wrong program" flow), `feedbackDelete` (button - discards the cycle without confirming it)
144
+ - Covered by a new test file (`tests/test_vis_feedback_actions.js`, 17 cases) plus an extension to `test_english_only.js`'s existing state/migration-coverage invariant check (the ten new state ids are brand new, so correctly added to the "nothing to migrate" allow-list rather than the migrations object)
145
+
135
146
  ### 0.4.37 (2026-10-03)
136
147
  - Fix: three test files added in recent releases (`test_show_probable_program.js`, `test_cycle_finish_exception_guard.js`, `test_cycle_boundary_reset.js`) called the bare global `setTimeout()`/`setInterval()` directly in their fake-adapter mocks - the same mistake made and fixed before in other test files, missed again when writing these new ones. Brought in line with the native-timer-alias pattern used everywhere else in this suite (review checker E5004/E5005)
137
148
  - Fix: `@iobroker/testing` devDependency bumped to `^6.3.0` (current; was `^6.2.2`)
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "laundrylens",
4
- "version": "0.4.37",
4
+ "version": "0.4.39",
5
5
  "news": {
6
+ "0.4.39": {
7
+ "en": "Fix: elapsedTime stayed frozen (often at a stale value left over from a previous cycle) for the entire \"detecting...\" period of a new cycle, only starting to update once a program was actually confirmed. elapsedTime is just Date.now() minus the cycle's start time though, with no real dependency on program detection - the callback that writes it was simply skipped entirely whenever no program had been recognized yet. Now updates from the moment a cycle starts running, regardless of detection status.",
8
+ "de": "Fix: elapsedTime blieb waehrend der gesamten \"detecting...\"-Phase eines neuen Zyklus eingefroren (oft auf einem veralteten Wert vom vorherigen Zyklus) und aktualisierte sich erst, sobald ein Programm tatsaechlich bestaetigt war. elapsedTime ist aber nur Date.now() minus Zyklusstartzeit, ohne echte Abhaengigkeit von der Programmerkennung - der Callback, der es schreibt, wurde schlicht komplett uebersprungen, solange noch kein Programm erkannt war. Aktualisiert sich jetzt ab dem Moment, in dem ein Zyklus zu laufen beginnt, unabhaengig vom Erkennungsstatus.",
9
+ "ru": "Исправление: elapsedTime оставался замороженным (часто на устаревшем значении с предыдущего цикла) на протяжении всего периода \"detecting...\" нового цикла и начинал обновляться только после фактического подтверждения программы. Однако elapsedTime - это просто Date.now() минус время начала цикла, без реальной зависимости от распознавания программы - колбэк, который его записывал, просто полностью пропускался, пока программа ещё не была распознана. Теперь обновляется с момента начала работы цикла, независимо от статуса распознавания.",
10
+ "pt": "Correcao: elapsedTime permanecia congelado (muitas vezes num valor obsoleto deixado do ciclo anterior) durante todo o periodo \"detecting...\" de um novo ciclo, so comecando a atualizar quando um programa era realmente confirmado. No entanto, elapsedTime e apenas Date.now() menos o horario de inicio do ciclo, sem dependencia real da deteccao de programa - o callback que o escrevia era simplesmente ignorado por completo enquanto nenhum programa tivesse sido reconhecido ainda. Agora atualiza a partir do momento em que um ciclo comeca a rodar, independente do status de deteccao.",
11
+ "nl": "Fix: elapsedTime bleef bevroren (vaak op een verouderde waarde van een vorige cyclus) gedurende de hele \"detecting...\"-periode van een nieuwe cyclus en werd pas bijgewerkt zodra een programma daadwerkelijk bevestigd was. elapsedTime is echter gewoon Date.now() min de startijd van de cyclus, zonder echte afhankelijkheid van programmaherkenning - de callback die het schreef werd simpelweg volledig overgeslagen zolang nog geen programma herkend was. Wordt nu bijgewerkt vanaf het moment dat een cyclus begint te lopen, ongeacht de herkenningsstatus.",
12
+ "fr": "Correction : elapsedTime restait fige (souvent sur une valeur obsolete laissee par un cycle precedent) pendant toute la periode \"detecting...\" d'un nouveau cycle, ne commencant a se mettre a jour qu'une fois un programme reellement confirme. Pourtant, elapsedTime n'est que Date.now() moins l'heure de debut du cycle, sans veritable dependance a la detection du programme - le callback qui l'ecrivait etait simplement totalement ignore tant qu'aucun programme n'avait encore ete reconnu. Se met desormais a jour des l'instant ou un cycle commence a tourner, quel que soit l'etat de detection.",
13
+ "it": "Correzione: elapsedTime restava bloccato (spesso su un valore obsoleto lasciato da un ciclo precedente) per l'intero periodo \"detecting...\" di un nuovo ciclo, iniziando ad aggiornarsi solo una volta che un programma veniva effettivamente confermato. Tuttavia elapsedTime e solo Date.now() meno l'orario di inizio del ciclo, senza una reale dipendenza dal rilevamento del programma - il callback che lo scriveva veniva semplicemente saltato del tutto finche non era stato riconosciuto alcun programma. Ora si aggiorna dal momento in cui un ciclo inizia a funzionare, indipendentemente dallo stato di rilevamento.",
14
+ "es": "Correccion: elapsedTime permanecia congelado (a menudo en un valor obsoleto de un ciclo anterior) durante todo el periodo \"detecting...\" de un nuevo ciclo, y solo empezaba a actualizarse una vez que se confirmaba realmente un programa. Sin embargo, elapsedTime es solo Date.now() menos la hora de inicio del ciclo, sin una dependencia real de la deteccion del programa - el callback que lo escribia simplemente se saltaba por completo mientras no se hubiera reconocido ningun programa todavia. Ahora se actualiza desde el momento en que un ciclo empieza a ejecutarse, independientemente del estado de deteccion.",
15
+ "pl": "Poprawka: elapsedTime pozostawal zamrozony (czesto na nieaktualnej wartosci pozostalej z poprzedniego cyklu) przez caly okres \"detecting...\" nowego cyklu i zaczynal sie aktualizowac dopiero po faktycznym potwierdzeniu programu. Jednak elapsedTime to po prostu Date.now() minus czas rozpoczecia cyklu, bez rzeczywistej zaleznosci od wykrywania programu - wywolanie zwrotne, ktore je zapisywalo, bylo po prostu calkowicie pomijane, dopoki zaden program nie zostal jeszcze rozpoznany. Teraz aktualizuje sie od momentu rozpoczecia dzialania cyklu, niezaleznie od statusu wykrywania.",
16
+ "uk": "Виправлення: elapsedTime залишався замороженим (часто на застарілому значенні з попереднього циклу) протягом усього періоду \"detecting...\" нового циклу і починав оновлюватися лише після фактичного підтвердження програми. Проте elapsedTime - це просто Date.now() мінус час початку циклу, без реальної залежності від розпізнавання програми - зворотний виклик, який його записував, просто повністю пропускався, доки програму ще не було розпізнано. Тепер оновлюється з моменту початку роботи циклу, незалежно від статусу розпізнавання.",
17
+ "zh-cn": "修复:在新周期的整个「detecting...」(检测中)阶段,elapsedTime 会一直冻结(通常停留在上一个周期遗留的过期值上),只有在程序被真正确认后才开始更新。然而 elapsedTime 其实只是 Date.now() 减去周期开始时间,与程序检测并无真正关联——写入它的回调函数在尚未识别出程序时会被完全跳过。现在从周期开始运行的那一刻起就会更新,与检测状态无关。"
18
+ },
19
+ "0.4.38": {
20
+ "en": "New: the Lernkontrolle (learning review/feedback) flow can now be driven from VIS, not just the admin tab. New read-only data points: pendingFeedbackCount, pendingFeedback (JSON list of all unconfirmed cycles, oldest first), and feedbackCycleId/feedbackProgram/feedbackDuration/feedbackEnergy/feedbackConfidence (convenience fields for the oldest one). New writable actions, each acting on the oldest pending cycle: feedbackConfirm (button, confirms it), feedbackCorrectProgram (dropdown of program names - writing one corrects and confirms), feedbackDelete (button, discards it without confirming).",
21
+ "de": "Neu: Die Lernkontrolle kann jetzt auch aus VIS heraus bedient werden, nicht nur ueber den Admin-Tab. Neue schreibgeschuetzte Datenpunkte: pendingFeedbackCount, pendingFeedback (JSON-Liste aller unbestaetigten Zyklen, aelteste zuerst) sowie feedbackCycleId/feedbackProgram/feedbackDuration/feedbackEnergy/feedbackConfidence (Komfortfelder fuer den aeltesten). Neue schreibbare Aktionen, die jeweils auf den aeltesten offenen Zyklus wirken: feedbackConfirm (Button, bestaetigt ihn), feedbackCorrectProgram (Dropdown mit Programmnamen - Eintrag schreiben korrigiert und bestaetigt), feedbackDelete (Button, verwirft ihn ohne Bestaetigung).",
22
+ "ru": "Новое: процесс контроля обучения (Lernkontrolle) теперь можно выполнять из VIS, а не только через вкладку администратора. Новые датапоинты только для чтения: pendingFeedbackCount, pendingFeedback (JSON-список всех неподтверждённых циклов, сначала самые старые) и feedbackCycleId/feedbackProgram/feedbackDuration/feedbackEnergy/feedbackConfidence (удобные поля для самого старого). Новые доступные для записи действия, каждое воздействует на самый старый ожидающий цикл: feedbackConfirm (кнопка, подтверждает), feedbackCorrectProgram (выпадающий список названий программ - запись исправляет и подтверждает), feedbackDelete (кнопка, отклоняет без подтверждения).",
23
+ "pt": "Novo: o fluxo de revisao de aprendizado (Lernkontrolle) agora pode ser controlado pelo VIS, nao apenas pela aba de administracao. Novos data points somente leitura: pendingFeedbackCount, pendingFeedback (lista JSON de todos os ciclos nao confirmados, do mais antigo ao mais novo) e feedbackCycleId/feedbackProgram/feedbackDuration/feedbackEnergy/feedbackConfidence (campos de conveniencia para o mais antigo). Novas acoes graváveis, cada uma atuando no ciclo pendente mais antigo: feedbackConfirm (botao, confirma), feedbackCorrectProgram (lista suspensa de nomes de programas - escrever um corrige e confirma), feedbackDelete (botao, descarta sem confirmar).",
24
+ "nl": "Nieuw: de Lernkontrolle (leercontrole/feedback) kan nu ook vanuit VIS worden bediend, niet alleen via het admin-tabblad. Nieuwe alleen-lezen datapoints: pendingFeedbackCount, pendingFeedback (JSON-lijst van alle onbevestigde cycli, oudste eerst) en feedbackCycleId/feedbackProgram/feedbackDuration/feedbackEnergy/feedbackConfidence (gemaksvelden voor de oudste). Nieuwe schrijfbare acties, elk werkend op de oudste openstaande cyclus: feedbackConfirm (knop, bevestigt deze), feedbackCorrectProgram (dropdown met programmanamen - schrijven corrigeert en bevestigt), feedbackDelete (knop, verwerpt zonder te bevestigen).",
25
+ "fr": "Nouveau : le flux de controle d'apprentissage (Lernkontrolle) peut desormais etre pilote depuis VIS, pas seulement via l'onglet admin. Nouveaux points de donnees en lecture seule : pendingFeedbackCount, pendingFeedback (liste JSON de tous les cycles non confirmes, du plus ancien au plus recent) et feedbackCycleId/feedbackProgram/feedbackDuration/feedbackEnergy/feedbackConfidence (champs pratiques pour le plus ancien). Nouvelles actions inscriptibles, agissant chacune sur le cycle en attente le plus ancien : feedbackConfirm (bouton, le confirme), feedbackCorrectProgram (liste deroulante de noms de programmes - ecrire une valeur corrige et confirme), feedbackDelete (bouton, l'ecarte sans confirmer).",
26
+ "it": "Novita: il flusso di controllo dell'apprendimento (Lernkontrolle) puo ora essere gestito da VIS, non solo tramite la scheda admin. Nuovi data point di sola lettura: pendingFeedbackCount, pendingFeedback (elenco JSON di tutti i cicli non confermati, dal piu vecchio) e feedbackCycleId/feedbackProgram/feedbackDuration/feedbackEnergy/feedbackConfidence (campi comodi per il piu vecchio). Nuove azioni scrivibili, ciascuna agisce sul ciclo in sospeso piu vecchio: feedbackConfirm (pulsante, lo conferma), feedbackCorrectProgram (menu a tendina dei nomi dei programmi - scrivere un valore corregge e conferma), feedbackDelete (pulsante, lo scarta senza confermare).",
27
+ "es": "Novedad: el flujo de control de aprendizaje (Lernkontrolle) ahora se puede manejar desde VIS, no solo desde la pestana de administracion. Nuevos data points de solo lectura: pendingFeedbackCount, pendingFeedback (lista JSON de todos los ciclos no confirmados, el mas antiguo primero) y feedbackCycleId/feedbackProgram/feedbackDuration/feedbackEnergy/feedbackConfidence (campos de conveniencia para el mas antiguo). Nuevas acciones escribibles, cada una actuando sobre el ciclo pendiente mas antiguo: feedbackConfirm (boton, lo confirma), feedbackCorrectProgram (lista desplegable de nombres de programas - escribir uno corrige y confirma), feedbackDelete (boton, lo descarta sin confirmar).",
28
+ "pl": "Nowosc: proces kontroli uczenia (Lernkontrolle) mozna teraz obslugiwac z poziomu VIS, a nie tylko przez zakladke administratora. Nowe datapointy tylko do odczytu: pendingFeedbackCount, pendingFeedback (lista JSON wszystkich niepotwierdzonych cykli, od najstarszego) oraz feedbackCycleId/feedbackProgram/feedbackDuration/feedbackEnergy/feedbackConfidence (wygodne pola dla najstarszego). Nowe zapisywalne akcje, kazda dziala na najstarszym oczekujacym cyklu: feedbackConfirm (przycisk, potwierdza go), feedbackCorrectProgram (lista rozwijana nazw programow - wpisanie poprawia i potwierdza), feedbackDelete (przycisk, odrzuca bez potwierdzania).",
29
+ "uk": "Нове: процес контролю навчання (Lernkontrolle) тепер можна виконувати з VIS, а не лише через вкладку адміністратора. Нові датапоінти лише для читання: pendingFeedbackCount, pendingFeedback (JSON-список усіх непідтверджених циклів, спочатку найстаріші) та feedbackCycleId/feedbackProgram/feedbackDuration/feedbackEnergy/feedbackConfidence (зручні поля для найстарішого). Нові записувані дії, кожна діє на найстаріший очікуючий цикл: feedbackConfirm (кнопка, підтверджує його), feedbackCorrectProgram (випадний список назв програм - запис виправляє і підтверджує), feedbackDelete (кнопка, відхиляє без підтвердження).",
30
+ "zh-cn": "新增:学习检查(Lernkontrolle)流程现在也可以从 VIS 操作,而不仅限于管理标签页。新增只读数据点:pendingFeedbackCount、pendingFeedback(所有未确认周期的 JSON 列表,最旧的在前)以及 feedbackCycleId/feedbackProgram/feedbackDuration/feedbackEnergy/feedbackConfidence(针对最旧周期的便捷字段)。新增可写操作,均作用于最旧的待处理周期:feedbackConfirm(按钮,确认该周期)、feedbackCorrectProgram(程序名称下拉列表——写入即可更正并确认)、feedbackDelete(按钮,不确认直接丢弃该周期)。"
31
+ },
6
32
  "0.4.37": {
7
33
  "en": "Fix: three test files added in recent releases (test_show_probable_program.js, test_cycle_finish_exception_guard.js, test_cycle_boundary_reset.js) called the bare global setTimeout()/setInterval() directly in their fake-adapter mocks, which the review checker's plain-timer-usage check flags - brought in line with the native-timer-alias pattern already used everywhere else in this test suite. Fix: bumped the @iobroker/testing devDependency to the current version (6.3.0).",
8
34
  "de": "Fix: drei in juengeren Releases hinzugefuegte Testdateien (test_show_probable_program.js, test_cycle_finish_exception_guard.js, test_cycle_boundary_reset.js) riefen in ihren Fake-Adapter-Mocks das nackte globale setTimeout()/setInterval() direkt auf, was die Pruefung des Review-Checkers auf rohe Timer-Aufrufe meldet - an das in dieser Testsuite ueberall sonst verwendete native-Timer-Alias-Muster angeglichen. Fix: die devDependency @iobroker/testing auf die aktuelle Version (6.3.0) angehoben.",
@@ -67,32 +93,6 @@
67
93
  "pl": "Poprawka: nowy cykl mógł przez kilka minut pokazywać pozostałość przypuszczenia programu z *poprzedniego*, już zakończonego cyklu (np. \"~30 Speed (76%)\" zaraz na starcie cyklu) - wewnętrzne pole najlepszego kandydata nie było czyszczone przy starcie nowego cyklu. Poprawka: cycleProgress pozostawał na ostatniej wartości (często 100%) po zakończeniu cyklu zamiast wrócić do 0. Poprawka: wyjątek podczas przetwarzania końcowego cyklu (zwłaszcza analiza post-hoc faz dla pralek/zmywarek) mógł po cichu blokować aktualizację datapointów statusu na żywo. Nowa opcja \"Pokaż prawdopodobny program przed potwierdzeniem\": opcjonalnie pokazuje bieżące przypuszczenie najlepszego kandydata w datapointach program/programText/confidence (z przedrostkiem \"≈\"), nie tylko we własnym podglądzie zakładki administratora. Nowa opcja \"Datapointy tekstowe bez emoji\": opcjonalnie pomija emoji w phaseText/stateText, na potrzeby dashboardów lub syntezy mowy.",
68
94
  "uk": "Виправлення: новий цикл міг кілька хвилин показувати залишкове припущення ймовірної програми з *попереднього*, уже завершеного циклу (наприклад, \"~30 Speed (76%)\" одразу на старті циклу) - внутрішнє поле найкращого кандидата не скидалося під час старту нового циклу. Виправлення: cycleProgress залишався на останньому значенні (часто 100%) після завершення циклу замість скидання до 0. Виправлення: виняток під час постобробки наприкінці циклу (зокрема постфактум-аналіз фаз для пральних/посудомийних машин) міг непомітно блокувати оновлення датапоінтів живого статусу. Нова опція \"Показувати ймовірну програму до підтвердження\": опціонально виводить поточне припущення найкращого кандидата в датапоінти program/programText/confidence (з префіксом \"≈\"), а не лише у власному прев'ю вкладки адміністратора. Нова опція \"Текстові датапоінти без емодзі\": опціонально прибирає емодзі з phaseText/stateText для дашбордів або синтезу мовлення.",
69
95
  "zh-cn": "修复:新周期可能会在最初几分钟内显示来自*上一个*已结束周期残留的可能程序猜测(例如周期刚开始就显示「~30 Speed (76%)」)——内部的最佳候选字段在新周期开始时未被清空。修复:周期结束后 cycleProgress 停留在最后的值(通常是 100%)而不会重置为 0。修复:周期结束后处理过程中的异常(尤其是洗衣机/洗碗机的事后阶段分析)可能会悄悄阻止实时状态数据点的更新。新增选项「确认前显示可能的程序」:可选择将实时最佳候选猜测显示到 program/programText/confidence 数据点中(带「≈」前缀),而不仅仅是管理标签页自身的预览。新增选项「文本数据点不含表情符号」:可选择在 phaseText/stateText 中省略表情符号,便于仪表盘或文本转语音使用。"
70
- },
71
- "0.4.32": {
72
- "en": "Follow-up fix to 0.4.31: a restart alone did not clear an already-stale state data point (e.g. stuck on \"Running\"), even with 0.4.31 installed. onReady() only re-writes the state/stateText/running/program/phase data points when a restore branch fires (sensor currently active, or a saved cycle needs resuming/finishing) - with neither true, the common case, nothing touched them at all. Now every device's startup unconditionally re-syncs these data points with the manager's actual resolved state, so a restart always clears any stale value immediately, regardless of cause.",
73
- "de": "Folgefix zu 0.4.31: ein reiner Neustart raeumte einen bereits haengengebliebenen Status-Datenpunkt (z.B. festgefahren auf \"Running\") nicht auf, selbst mit installiertem 0.4.31. onReady() schrieb die Datenpunkte state/stateText/running/program/phase bisher nur neu, wenn ein Restore-Zweig griff (Sensor aktuell aktiv, oder ein gespeicherter Zyklus muss fortgesetzt/beendet werden) - traf keins von beidem zu, der Normalfall, wurde nichts angefasst. Jetzt synchronisiert jeder Geraete-Start diese Datenpunkte bedingungslos mit dem tatsaechlichen, aufgeloesten Zustand des Managers, sodass ein Neustart jeden haengengebliebenen Wert sofort beseitigt, unabhaengig von der Ursache.",
74
- "ru": "Дополнительное исправление к 0.4.31: одна лишь перезагрузка не устраняла уже зависший датапоинт состояния (например, застрявший на \"Running\"), даже при установленном 0.4.31. onReady() перезаписывал датапоинты state/stateText/running/program/phase только при срабатывании ветки восстановления (датчик сейчас активен либо сохранённый цикл нужно возобновить/завершить) - если ни то, ни другое не верно (обычный случай), ничего не менялось. Теперь при каждом запуске устройства эти датапоинты безусловно синхронизируются с фактическим состоянием менеджера, поэтому перезапуск всегда немедленно устраняет любое зависшее значение, независимо от причины.",
75
- "pt": "Correcao de acompanhamento a 0.4.29: reiniciar sozinho nao limpava um data point de estado ja obsoleto (por exemplo, preso em \"Running\"), mesmo com a 0.4.31 instalada. O onReady() so reescrevia os data points state/stateText/running/program/phase quando um ramo de restauracao era acionado (sensor atualmente ativo, ou um ciclo salvo precisa ser retomado/finalizado) - sem nenhum dos dois, o caso comum, nada era alterado. Agora a inicializacao de cada dispositivo sincroniza incondicionalmente esses data points com o estado real resolvido do gerenciador, entao reiniciar sempre limpa qualquer valor obsoleto imediatamente, independente da causa.",
76
- "nl": "Vervolgfix op 0.4.31: een herstart alleen wiste een reeds verouderd state-datapoint (bijv. vastgelopen op \"Running\") niet, zelfs met 0.4.31 geinstalleerd. onReady() herschreef de datapoints state/stateText/running/program/phase alleen wanneer een herstel-tak werd geactiveerd (sensor momenteel actief, of een opgeslagen cyclus moet worden hervat/afgerond) - was geen van beide waar, het normale geval, dan werd er niets aangepast. Nu synchroniseert elke apparaatstart deze datapoints onvoorwaardelijk met de daadwerkelijk vastgestelde status van de manager, zodat een herstart elke verouderde waarde altijd meteen opheldert, ongeacht de oorzaak.",
77
- "fr": "Correction complementaire a 0.4.31 : un simple redemarrage ne nettoyait pas un data point d'etat deja obsolete (par ex. bloque sur \"Running\"), meme avec la 0.4.31 installee. onReady() ne reecrivait les data points state/stateText/running/program/phase que lorsqu'une branche de restauration se declenchait (capteur actuellement actif, ou un cycle enregistre doit etre repris/termine) - si aucun des deux n'etait vrai, le cas courant, rien n'etait modifie. Desormais, chaque demarrage d'appareil resynchronise inconditionnellement ces data points avec l'etat reel resolu du gestionnaire, de sorte qu'un redemarrage efface toujours immediatement toute valeur obsolete, quelle qu'en soit la cause.",
78
- "it": "Correzione di follow-up alla 0.4.31: un semplice riavvio non ripuliva un data point di stato gia bloccato (ad es. fermo su \"Running\"), anche con la 0.4.31 installata. onReady() riscriveva i data point state/stateText/running/program/phase solo quando si attivava un ramo di ripristino (sensore attualmente attivo, oppure un ciclo salvato deve essere ripreso/terminato) - se nessuno dei due era vero, il caso comune, non veniva modificato nulla. Ora l'avvio di ogni dispositivo risincronizza incondizionatamente questi data point con lo stato effettivo risolto del manager, cosi un riavvio ripulisce sempre immediatamente qualsiasi valore bloccato, indipendentemente dalla causa.",
79
- "es": "Correccion de seguimiento a 0.4.31: reiniciar por si solo no limpiaba un data point de estado ya obsoleto (por ejemplo, atascado en \"Running\"), incluso con 0.4.31 instalado. onReady() solo reescribia los data points state/stateText/running/program/phase cuando se activaba una rama de restauracion (sensor actualmente activo, o un ciclo guardado necesita reanudarse/finalizarse) - si ninguno de los dos era cierto, el caso comun, no se tocaba nada. Ahora el arranque de cada dispositivo resincroniza incondicionalmente estos data points con el estado real resuelto del gestor, por lo que reiniciar siempre limpia de inmediato cualquier valor atascado, sin importar la causa.",
80
- "pl": "Poprawka uzupelniajaca do 0.4.31: samo ponowne uruchomienie nie czyscilo juz nieaktualnego datapointu stanu (np. utknietego na \"Running\"), nawet po zainstalowaniu 0.4.31. onReady() ponownie zapisywal datapointy state/stateText/running/program/phase tylko wtedy, gdy uruchamiala sie galaz przywracania (czujnik aktualnie aktywny lub zapisany cykl wymaga wznowienia/zakonczenia) - jesli zadne z tych warunkow nie bylo spelnione, czyli w typowym przypadku, nic nie bylo zmieniane. Teraz uruchomienie kazdego urzadzenia bezwarunkowo synchronizuje te datapointy z faktycznym, rozwiazanym stanem menedzera, dzieki czemu ponowne uruchomienie zawsze natychmiast czysci kazda utkniete wartosc, niezaleznie od przyczyny.",
81
- "uk": "Додаткове виправлення до 0.4.31: сама лише перезагрузка не очищала вже застарілий датапоінт стану (наприклад, застряглий на \"Running\"), навіть із встановленим 0.4.31. onReady() перезаписував датапоінти state/stateText/running/program/phase лише коли спрацьовувала гілка відновлення (датчик наразі активний, або збережений цикл потрібно відновити/завершити) - якщо жодне з цього не було правдою, звичайний випадок, нічого не змінювалося. Тепер запуск кожного пристрою безумовно синхронізує ці датапоінти з фактичним, визначеним станом менеджера, тому перезапуск завжди негайно очищує будь-яке застарile значення, незалежно від причини.",
82
- "zh-cn": "0.4.31 的后续修复:即使安装了 0.4.31,仅重启也无法清除已经过期的状态数据点(例如卡在「Running」)。此前 onReady() 只有在触发恢复分支时(传感器当前处于活动状态,或需要恢复/结束已保存的周期)才会重写 state/stateText/running/program/phase 数据点——如果两者都不成立(这是常见情况),则完全不做任何更改。现在每台设备启动时都会无条件将这些数据点与管理器实际解析出的状态同步,因此重启总能立即清除任何过期的值,无论原因是什么。"
83
- },
84
- "0.4.31": {
85
- "en": "Fix: a real bug report - the admin tab showed the dryer as off while the state data point still read \"Running\". Root cause: the dryer's anti-crease \"power drop\" quick-finish path (which force-ends a cycle 45s after a sudden power drop, to react faster than waiting for the full off-delay) set the in-memory state to off directly but never notified the rest of the adapter about it - so the state/stateText/running/program/phase data points stayed frozen on their last value from before the drop, even though the live status the admin tab reads was already correct. Fixed by firing the same notification the normal end-of-cycle transition already fires. If you're seeing this right now, restarting the adapter clears the stuck values immediately without waiting for the next cycle.",
86
- "de": "Fix: ein realer Fehlerbericht - der Admin-Tab zeigte den Trockner als aus an, waehrend der Status-Datenpunkt noch \"Running\" anzeigte. Ursache: der Anti-Knitter-Schnellabschluss des Trockners (der einen Zyklus 45 Sekunden nach einem ploetzlichen Leistungsabfall zwangsbeendet, um schneller zu reagieren als die volle Ausschaltverzoegerung abzuwarten) setzte den internen Status direkt auf aus, benachrichtigte aber nie den Rest des Adapters darueber - daher blieben die Datenpunkte state/stateText/running/program/phase auf ihrem letzten Wert vor dem Abfall eingefroren, obwohl der Live-Status, den der Admin-Tab liest, bereits korrekt war. Behoben, indem dieselbe Benachrichtigung ausgeloest wird, die der normale Zyklusende-Uebergang ohnehin schon ausloest. Wer das gerade sieht: ein Neustart des Adapters raeumt die haengengebliebenen Werte sofort auf, ohne auf den naechsten Zyklus warten zu muessen.",
87
- "ru": "Исправление: реальный отчёт об ошибке - вкладка администратора показывала сушилку выключенной, тогда как датапоинт состояния всё ещё показывал \"Running\". Причина: путь быстрого завершения цикла сушилки при защите от сминания (\"power drop\" - принудительно завершает цикл через 45 секунд после резкого падения мощности, чтобы реагировать быстрее полной задержки отключения) устанавливал внутреннее состояние в off напрямую, но никогда не уведомлял остальную часть адаптера об этом - поэтому датапоинты state/stateText/running/program/phase оставались замороженными на последнем значении до падения, хотя живой статус, который читает вкладка администратора, уже был корректен. Исправлено вызовом того же уведомления, которое уже вызывает обычный переход в конце цикла. Если вы видите это прямо сейчас, перезапуск адаптера немедленно устраняет зависшие значения без ожидания следующего цикла.",
88
- "pt": "Correcao: um relato real de bug - a aba de administracao mostrava a secadora como desligada enquanto o data point de estado ainda mostrava \"Running\". Causa raiz: o caminho de finalizacao rapida por queda de energia (anti-amarrotamento) da secadora (que forca o fim de um ciclo 45s apos uma queda subita de energia, para reagir mais rapido do que esperar o atraso total de desligamento) definia o estado interno como desligado diretamente, mas nunca notificava o restante do adaptador sobre isso - entao os data points state/stateText/running/program/phase ficavam congelados no ultimo valor antes da queda, mesmo que o status ao vivo lido pela aba de administracao ja estivesse correto. Corrigido disparando a mesma notificacao que a transicao normal de fim de ciclo ja dispara. Se voce esta vendo isso agora, reiniciar o adaptador limpa os valores presos imediatamente, sem esperar pelo proximo ciclo.",
89
- "nl": "Fix: een echte bugmelding - het admin-tabblad toonde de droger als uit terwijl het state-datapoint nog \"Running\" liet zien. Oorzaak: het kreukbeschermings-\"stroomval\"-snelafsluitpad van de droger (dat een cyclus 45s na een plotselinge stroomval geforceerd beeindigt om sneller te reageren dan de volledige uitschakelvertraging) zette de interne status direct op uit, maar informeerde de rest van de adapter daar nooit over - waardoor de datapoints state/stateText/running/program/phase bevroren bleven op hun laatste waarde van voor de val, terwijl de live status die het admin-tabblad leest al correct was. Opgelost door dezelfde melding te versturen die de normale cyclus-einde-overgang al verstuurt. Zie je dit nu, dan lost een herstart van de adapter de vastzittende waarden meteen op, zonder op de volgende cyclus te hoeven wachten.",
90
- "fr": "Correction : un vrai signalement de bug - l'onglet admin affichait le secheur comme eteint alors que le data point d'etat indiquait encore \"Running\". Cause : le chemin de fin rapide anti-froissage du secheur (qui force la fin d'un cycle 45s apres une chute soudaine de puissance, pour reagir plus vite que d'attendre le delai d'arret complet) mettait l'etat interne a off directement, mais ne notifiait jamais le reste de l'adaptateur - les data points state/stateText/running/program/phase restaient donc figes sur leur derniere valeur d'avant la chute, alors que le statut en direct lu par l'onglet admin etait deja correct. Corrige en declenchant la meme notification que la transition normale de fin de cycle declenche deja. Si vous voyez ce cas maintenant, redemarrer l'adaptateur efface immediatement les valeurs bloquees sans attendre le prochain cycle.",
91
- "it": "Correzione: una segnalazione reale di bug - la scheda admin mostrava l'asciugatrice come spenta mentre il data point di stato mostrava ancora \"Running\". Causa: il percorso di chiusura rapida anti-piega dell'asciugatrice (che forza la fine di un ciclo 45s dopo un calo improvviso di potenza, per reagire piu velocemente rispetto ad attendere il ritardo di spegnimento completo) impostava lo stato interno su off direttamente, ma non notificava mai il resto dell'adattatore - quindi i data point state/stateText/running/program/phase restavano bloccati sull'ultimo valore prima del calo, anche se lo stato live letto dalla scheda admin era gia corretto. Risolto attivando la stessa notifica gia attivata dalla normale transizione di fine ciclo. Se lo state vedendo ora, riavviare l'adattatore risolve subito i valori bloccati senza attendere il ciclo successivo.",
92
- "es": "Correccion: un informe real de error - la pestana de administracion mostraba la secadora como apagada mientras el data point de estado aun mostraba \"Running\". Causa raiz: el camino de finalizacion rapida anti-arrugas de la secadora (que fuerza el fin de un ciclo 45s despues de una caida repentina de potencia, para reaccionar mas rapido que esperar el retraso de apagado completo) establecia el estado interno en apagado directamente, pero nunca notificaba al resto del adaptador - por lo que los data points state/stateText/running/program/phase quedaban congelados en su ultimo valor antes de la caida, aunque el estado en vivo que lee la pestana de administracion ya era correcto. Corregido disparando la misma notificacion que ya dispara la transicion normal de fin de ciclo. Si esta viendo esto ahora mismo, reiniciar el adaptador limpia los valores atascados de inmediato, sin esperar al proximo ciclo.",
93
- "pl": "Poprawka: prawdziwe zgloszenie bledu - zakladka administratora pokazywala suszarke jako wylaczona, podczas gdy datapoint stanu nadal pokazywal \"Running\". Przyczyna: sciezka szybkiego zakonczenia cyklu suszarki przy ochronie przed gnieceniem (\"power drop\" - wymusza zakonczenie cyklu 45s po nagłym spadku mocy, aby zareagowac szybciej niz czekajac na pelne opoznienie wylaczenia) ustawiala stan wewnetrzny na wylaczony bezposrednio, ale nigdy nie powiadamiala o tym reszty adaptera - dlatego datapointy state/stateText/running/program/phase pozostawaly zamrozone na ostatniej wartosci sprzed spadku, mimo ze status na zywo odczytywany przez zakladke administratora byl juz poprawny. Naprawiono, wywolujac to samo powiadomienie, ktore wywoluje juz normalne przejscie konca cyklu. Jesli widzisz to teraz, ponowne uruchomienie adaptera natychmiast czysci utkniete wartosci bez czekania na kolejny cykl.",
94
- "uk": "Виправлення: реальне повідомлення про помилку - вкладка адміністратора показувала сушарку вимкненою, тоді як датапоінт стану все ще показував \"Running\". Причина: шлях швидкого завершення циклу сушарки при захисті від зминання (\"power drop\" - примусово завершує цикл через 45 секунд після різкого падіння потужності, щоб реагувати швидше за повне очікування затримки вимкнення) встановлював внутрішній стан на вимкнено напряму, але ніколи не повідомляв про це решту адаптера - тому датапоінти state/stateText/running/program/phase залишалися замороженими на останньому значенні до падіння, хоча живий статус, який читає вкладка адміністратора, вже був коректним. Виправлено викликом того самого сповіщення, яке вже викликає звичайний перехід у кінці циклу. Якщо ви бачите це зараз, перезапуск адаптера негайно очищує застряглі значення без очікування наступного циклу.",
95
- "zh-cn": "修复:一个真实的问题反馈——管理标签页显示烘干机已关闭,但状态数据点仍显示「Running」。根本原因:烘干机的防皱「功率骤降」快速结束路径(在功率突然下降 45 秒后强制结束周期,以比等待完整关闭延迟更快地做出反应)直接将内存中的状态设为关闭,但从未通知适配器的其余部分——因此 state/stateText/running/program/phase 数据点一直冻结在骤降前的最后一个值,尽管管理标签页读取的实时状态早已正确。修复方法是触发与正常周期结束转换相同的通知。如果你现在正遇到这个问题,重启适配器即可立即清除卡住的值,无需等待下一个周期。"
96
96
  }
97
97
  },
98
98
  "titleLang": {
package/main.js CHANGED
@@ -347,6 +347,7 @@ class WashdataAdapter extends utils.Adapter {
347
347
  onProgramChange: (program, confidence) =>
348
348
  this._onProgram(deviceCfg.deviceId, program, confidence),
349
349
  onTimeUpdate: (remaining, total, pct) => {
350
+ this._updateElapsedTime(deviceCfg.deviceId);
350
351
  if (remaining === null) {
351
352
  // No program detected – reset remaining time and progress
352
353
  this.setState(`${deviceCfg.deviceId}.timeRemaining`, 0, true);
@@ -378,6 +379,11 @@ class WashdataAdapter extends utils.Adapter {
378
379
  // Subscribe to writable states
379
380
  await this.subscribeStatesAsync(`${deviceCfg.deviceId}.programOverride`);
380
381
  await this.subscribeStatesAsync(`${deviceCfg.deviceId}.forceFinish`);
382
+ await this.subscribeStatesAsync(`${deviceCfg.deviceId}.feedbackConfirm`);
383
+ await this.subscribeStatesAsync(
384
+ `${deviceCfg.deviceId}.feedbackCorrectProgram`,
385
+ );
386
+ await this.subscribeStatesAsync(`${deviceCfg.deviceId}.feedbackDelete`);
381
387
 
382
388
  // Disable ghost protection if the sensor is currently drawing power
383
389
  try {
@@ -664,6 +670,36 @@ class WashdataAdapter extends utils.Adapter {
664
670
  }
665
671
  return;
666
672
  }
673
+ if (id === `${this.namespace}.${deviceId}.feedbackConfirm`) {
674
+ if (state.val === true) {
675
+ this._handleFeedbackConfirm(deviceId, mgr).catch((err) =>
676
+ this.log.error(`${deviceId}: feedbackConfirm: ${err.message}`),
677
+ );
678
+ this.setState(`${deviceId}.feedbackConfirm`, false, true);
679
+ }
680
+ return;
681
+ }
682
+ if (id === `${this.namespace}.${deviceId}.feedbackCorrectProgram`) {
683
+ if (state.val) {
684
+ this._handleFeedbackCorrectProgram(deviceId, mgr, state.val).catch(
685
+ (err) =>
686
+ this.log.error(
687
+ `${deviceId}: feedbackCorrectProgram: ${err.message}`,
688
+ ),
689
+ );
690
+ }
691
+ this.setState(`${deviceId}.feedbackCorrectProgram`, "", true);
692
+ return;
693
+ }
694
+ if (id === `${this.namespace}.${deviceId}.feedbackDelete`) {
695
+ if (state.val === true) {
696
+ this._handleFeedbackDelete(deviceId, mgr).catch((err) =>
697
+ this.log.error(`${deviceId}: feedbackDelete: ${err.message}`),
698
+ );
699
+ this.setState(`${deviceId}.feedbackDelete`, false, true);
700
+ }
701
+ return;
702
+ }
667
703
  }
668
704
  }
669
705
 
@@ -760,6 +796,84 @@ class WashdataAdapter extends utils.Adapter {
760
796
  mgr.detector.reset();
761
797
  }
762
798
 
799
+ // Returns the oldest cycle still needing feedback for this device, or
800
+ // null if there isn't one - the single target the three feedback*
801
+ // writable data points below act on, same cycle _updateNeedsFeedback()
802
+ // exposes via feedbackCycleId/feedbackProgram/etc.
803
+ _oldestPendingCycle(mgr) {
804
+ const open = mgr
805
+ .getCycleHistory()
806
+ .filter((c) => !c.confirmed)
807
+ .sort((a, b) => a.startTime - b.startTime);
808
+ return open[0] || null;
809
+ }
810
+
811
+ // VIS-friendly equivalent of the admin tab's confirmCycle sendTo
812
+ // handler (see onMessage() below), acting on the oldest pending cycle.
813
+ async _handleFeedbackConfirm(deviceId, mgr) {
814
+ const cycle = this._oldestPendingCycle(mgr);
815
+ if (!cycle) {
816
+ return;
817
+ }
818
+ cycle.confirmed = true;
819
+ if (cycle.profileId) {
820
+ mgr.profileStore.learnFromCycle(cycle.profileId, [], cycle.durationMs);
821
+ await mgr.profileStore.save();
822
+ }
823
+ await mgr._saveState();
824
+ await this._updateNeedsFeedback(deviceId, mgr);
825
+ }
826
+
827
+ // VIS-friendly equivalent of correctCycle, resolving the written
828
+ // program name back to a profile id the way programOverride's own
829
+ // handler (_handleProgramOverride) already does.
830
+ async _handleFeedbackCorrectProgram(deviceId, mgr, programName) {
831
+ const cycle = this._oldestPendingCycle(mgr);
832
+ if (!cycle) {
833
+ return;
834
+ }
835
+ const profile = mgr.profileStore
836
+ .getAllProfiles()
837
+ .find((p) => p.name === programName);
838
+ if (!profile) {
839
+ this.log.warn(
840
+ `${deviceId}: feedbackCorrectProgram: unknown program "${programName}"`,
841
+ );
842
+ return;
843
+ }
844
+ cycle.matchedProfile = profile.name;
845
+ cycle.profileId = profile.id;
846
+ cycle.confirmed = true;
847
+ cycle.corrected = true;
848
+ const traceForLearn = mgr.getTrace(cycle.id);
849
+ const tracePoints = traceForLearn
850
+ ? traceForLearn.points.map((p) => ({ ts: p.ts, watts: p.watts }))
851
+ : [];
852
+ mgr.profileStore.learnFromCycle(profile.id, tracePoints, cycle.durationMs);
853
+ await mgr.profileStore.save();
854
+ await mgr._saveState();
855
+ await this._updateNeedsFeedback(deviceId, mgr);
856
+ await this._updateOverrideStates(deviceId, mgr);
857
+ }
858
+
859
+ // VIS-friendly equivalent of deleteCycle, discarding the oldest
860
+ // pending cycle without confirming or correcting it.
861
+ async _handleFeedbackDelete(deviceId, mgr) {
862
+ const cycle = this._oldestPendingCycle(mgr);
863
+ if (!cycle) {
864
+ return;
865
+ }
866
+ const idx = mgr.cycleHistory.findIndex((c) => c.id === cycle.id);
867
+ if (idx === -1) {
868
+ return;
869
+ }
870
+ mgr.cycleHistory.splice(idx, 1);
871
+ mgr.traceStore.deleteTrace(cycle.id);
872
+ await mgr._saveState();
873
+ await mgr.traceStore.save();
874
+ await this._updateNeedsFeedback(deviceId, mgr);
875
+ }
876
+
763
877
  async onMessage(obj) {
764
878
  if (!obj || !obj.command) {
765
879
  return;
@@ -1419,16 +1533,76 @@ class WashdataAdapter extends utils.Adapter {
1419
1533
  JSON.stringify(profiles.map((p) => p.name)),
1420
1534
  true,
1421
1535
  );
1536
+ // feedbackCorrectProgram's own dropdown - same program list, also
1537
+ // without 'auto' (correcting a past, already-finished cycle to
1538
+ // "automatic" makes no sense the way it does for programOverride)
1539
+ await this.extendObjectAsync(`${deviceId}.feedbackCorrectProgram`, {
1540
+ common: {
1541
+ states: profiles.reduce((o, p) => {
1542
+ o[p.name] = p.name;
1543
+ return o;
1544
+ }, {}),
1545
+ },
1546
+ });
1422
1547
  }
1423
1548
 
1424
- // ── Update "feedback needed" indicator ──────────────────
1425
- // Mirrors admin/tab_m.html's updateFeedbackBadge() exactly (cycles
1426
- // with !confirmed) - that is the only place this count was ever
1427
- // computed before, entirely client-side, which is why the
1428
- // needsFeedback data point itself was never actually written.
1549
+ // ── Update "feedback needed" indicator + VIS-friendly pending-
1550
+ // feedback data points ──────────────────────────────────
1551
+ // Mirrors admin/tab_m.html's renderFeedback()/updateFeedbackBadge()
1552
+ // exactly (cycles with !confirmed, oldest first) - that used to be
1553
+ // the only place this was ever computed, entirely client-side via
1554
+ // sendTo, which is why none of this was ever exposed as data points
1555
+ // that a VIS dashboard (or any other automation) could read or act
1556
+ // on directly.
1429
1557
  async _updateNeedsFeedback(deviceId, mgr) {
1430
- const needsFeedback = mgr.getCycleHistory().some((c) => !c.confirmed);
1431
- this.setState(`${deviceId}.needsFeedback`, needsFeedback, true);
1558
+ const open = mgr
1559
+ .getCycleHistory()
1560
+ .filter((c) => !c.confirmed)
1561
+ .sort((a, b) => a.startTime - b.startTime);
1562
+
1563
+ this.setState(`${deviceId}.needsFeedback`, open.length > 0, true);
1564
+ this.setState(`${deviceId}.pendingFeedbackCount`, open.length, true);
1565
+ this.setState(
1566
+ `${deviceId}.pendingFeedback`,
1567
+ JSON.stringify(
1568
+ open.map((c) => ({
1569
+ id: c.id,
1570
+ startTime: c.startTime,
1571
+ program: c.matchedProfile || "",
1572
+ durationMin: Math.round((c.durationMs || 0) / 60000),
1573
+ energyWh: Math.round((c.energyWh || 0) * 100) / 100,
1574
+ confidencePct: Math.round((c.confidence || 0) * 100),
1575
+ })),
1576
+ ),
1577
+ true,
1578
+ );
1579
+
1580
+ // Convenience fields for the single oldest pending cycle - the one
1581
+ // feedbackConfirm/feedbackCorrectProgram/feedbackDelete act on - so
1582
+ // a simple VIS text/button widget doesn't need to parse the JSON
1583
+ // array above just to show "what am I confirming right now".
1584
+ const oldest = open[0];
1585
+ this.setState(`${deviceId}.feedbackCycleId`, oldest ? oldest.id : "", true);
1586
+ this.setState(
1587
+ `${deviceId}.feedbackProgram`,
1588
+ oldest ? oldest.matchedProfile || "" : "",
1589
+ true,
1590
+ );
1591
+ this.setState(
1592
+ `${deviceId}.feedbackDuration`,
1593
+ oldest ? Math.round((oldest.durationMs || 0) / 60000) : 0,
1594
+ true,
1595
+ );
1596
+ this.setState(
1597
+ `${deviceId}.feedbackEnergy`,
1598
+ oldest ? Math.round((oldest.energyWh || 0) * 100) / 100 : 0,
1599
+ true,
1600
+ );
1601
+ this.setState(
1602
+ `${deviceId}.feedbackConfidence`,
1603
+ oldest ? Math.round((oldest.confidence || 0) * 100) : 0,
1604
+ true,
1605
+ );
1432
1606
  }
1433
1607
 
1434
1608
  // ── Callbacks ────────────────────────────────────────────────
@@ -1538,6 +1712,22 @@ class WashdataAdapter extends utils.Adapter {
1538
1712
  }
1539
1713
  }
1540
1714
 
1715
+ // elapsedTime is purely Date.now() - cycleStartTime - meaningful and
1716
+ // available from the moment RUNNING starts, with no actual dependency
1717
+ // on whether a program has been detected yet. Called both from
1718
+ // onTimeUpdate()'s "no program detected" early-return branch and from
1719
+ // _onTime()'s normal path, so it keeps ticking up throughout the
1720
+ // entire "detecting..." period instead of only once a program is
1721
+ // confirmed - found live (reported stuck at a stale value, e.g. left
1722
+ // over from a previous, longer cycle, until recognition caught up).
1723
+ _updateElapsedTime(deviceId) {
1724
+ const mgr = this.managers[deviceId];
1725
+ if (mgr && mgr.cycleStartTime) {
1726
+ const elapsedSec = Math.round((Date.now() - mgr.cycleStartTime) / 1000);
1727
+ this.setState(`${deviceId}.elapsedTime`, elapsedSec, true);
1728
+ }
1729
+ }
1730
+
1541
1731
  _onTime(
1542
1732
  deviceId,
1543
1733
  remainingSeconds,
@@ -1598,12 +1788,7 @@ class WashdataAdapter extends utils.Adapter {
1598
1788
  ).catch(() => {});
1599
1789
  }
1600
1790
  this.setState(`${deviceId}.timeRemaining`, remainingSeconds ?? 0, true);
1601
- // Elapsed time from the manager
1602
- const mgr2 = this.managers[deviceId];
1603
- if (mgr2 && mgr2.cycleStartTime) {
1604
- const elapsedSec = Math.round((Date.now() - mgr2.cycleStartTime) / 1000);
1605
- this.setState(`${deviceId}.elapsedTime`, elapsedSec, true);
1606
- }
1791
+ this._updateElapsedTime(deviceId);
1607
1792
  this.setState(`${deviceId}.totalDuration`, totalSeconds ?? 0, true);
1608
1793
  // Only overwrite progress if > 0 (prevents a reset if the profile
1609
1794
  // briefly doesn't match) - UNLESS forceWrite is set, which is how
@@ -2240,6 +2425,67 @@ class WashdataAdapter extends utils.Adapter {
2240
2425
  def: false,
2241
2426
  write: false,
2242
2427
  },
2428
+ // Lernkontrolle (feedback) - VIS-friendly data points so the
2429
+ // confirm/correct/delete flow doesn't require the admin tab
2430
+ {
2431
+ id: "pendingFeedbackCount",
2432
+ name: "Cycles needing feedback",
2433
+ type: "number",
2434
+ role: "value",
2435
+ def: 0,
2436
+ write: false,
2437
+ },
2438
+ {
2439
+ id: "pendingFeedback",
2440
+ name: "Cycles needing feedback (JSON)",
2441
+ type: "string",
2442
+ role: "json",
2443
+ def: "[]",
2444
+ write: false,
2445
+ },
2446
+ {
2447
+ id: "feedbackCycleId",
2448
+ name: "Oldest pending cycle ID",
2449
+ type: "string",
2450
+ role: "text",
2451
+ def: "",
2452
+ write: false,
2453
+ },
2454
+ {
2455
+ id: "feedbackProgram",
2456
+ name: "Oldest pending cycle: detected program",
2457
+ type: "string",
2458
+ role: "text",
2459
+ def: "",
2460
+ write: false,
2461
+ },
2462
+ {
2463
+ id: "feedbackDuration",
2464
+ name: "Oldest pending cycle: duration",
2465
+ type: "number",
2466
+ role: "value",
2467
+ def: 0,
2468
+ write: false,
2469
+ unit: "min",
2470
+ },
2471
+ {
2472
+ id: "feedbackEnergy",
2473
+ name: "Oldest pending cycle: consumption",
2474
+ type: "number",
2475
+ role: "value",
2476
+ def: 0,
2477
+ write: false,
2478
+ unit: "Wh",
2479
+ },
2480
+ {
2481
+ id: "feedbackConfidence",
2482
+ name: "Oldest pending cycle: confidence",
2483
+ type: "number",
2484
+ role: "value",
2485
+ def: 0,
2486
+ write: false,
2487
+ unit: "%",
2488
+ },
2243
2489
  // Zusatz-Info
2244
2490
  {
2245
2491
  id: "phase",
@@ -2315,6 +2561,32 @@ class WashdataAdapter extends utils.Adapter {
2315
2561
  write: true,
2316
2562
  read: false,
2317
2563
  },
2564
+ {
2565
+ id: "feedbackConfirm",
2566
+ name: "Confirm oldest pending cycle",
2567
+ type: "boolean",
2568
+ role: "button",
2569
+ def: false,
2570
+ write: true,
2571
+ read: false,
2572
+ },
2573
+ {
2574
+ id: "feedbackCorrectProgram",
2575
+ name: "Correct oldest pending cycle to this program, then confirm",
2576
+ type: "string",
2577
+ role: "text",
2578
+ def: "",
2579
+ write: true,
2580
+ },
2581
+ {
2582
+ id: "feedbackDelete",
2583
+ name: "Delete oldest pending cycle without confirming",
2584
+ type: "boolean",
2585
+ role: "button",
2586
+ def: false,
2587
+ write: true,
2588
+ read: false,
2589
+ },
2318
2590
  ];
2319
2591
 
2320
2592
  for (const s of states) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.laundrylens",
3
- "version": "0.4.37",
3
+ "version": "0.4.39",
4
4
  "description": "ioBroker adapter that detects washing machine and dryer cycles via smart plug power measurement, matches them against learned programs and estimates remaining time",
5
5
  "author": {
6
6
  "name": "backfisch88",