Releases: backfisch88/ioBroker.laundrylens
Release list
Release v0.4.23
Follow-up to 0.4.22's offDelayMin fix, found by asking the user whether
anything unusual happened around the "kein Ende erkannt" incident - they
had restarted ioBroker in between. That's the missing piece.
Root cause: main.js's on-startup restore logic (onReady()) only had a
branch for "the sensor is still reading high power at restart" (resume
as running, restore the trace). There was no branch at all for "a
cycle was running before the restart, but power is now idle" - i.e.
the appliance actually finished while the adapter was offline. In that
case, the in-memory WashDataManager/CycleDetector just sat at their
constructor defaults (state: "off", no cycleStartTime): the previous
cycle's state/program/etc data points stayed frozen at whatever they
showed before the restart, and _onCycleFinished() was never called
for it - it was completely orphaned, staying stuck open forever with
no end ever recorded, exactly matching the user's screenshot (a cycle
whose power trace clearly dropped to ~0W around the 245-minute mark
but was still shown as an open, unconfirmed entry at the 327-minute
mark, matching real elapsed time at the moment of the screenshot).
Fix: added the missing branch. When a restored cycle exists but power
is currently idle, resume the state machine as "running" (restoring
the saved trace and cycleStartTime), seed lastAboveThreshold from the
trace's last point that was actually above threshold (falling back to
cycleStartTime), then feed the current low reading through
processPowerReading() - the normal public entry point used everywhere
else for per-reading processing. This lets the cycle proceed through
the ordinary PAUSED -> ENDING -> OFF path (using the real offDelayMin
now that 0.4.22 wired it correctly) and finish properly instead of
being orphaned.
The lastAboveThreshold seeding matters on its own: without it, the
ENDING -> OFF transition can never fire at all (it requires
lastAboveThreshold to compute elapsed off-time, and it's otherwise
null from the constructor) - confirmed by first reproducing the
partial fix (resume as running, but without seeding
lastAboveThreshold) getting permanently stuck in "ending".
Added tests/test_restart_resume_low_power.js:
- Confirms the without-fix behavior first (a restored cycle with idle
power silently stays "off" - i.e. gets forgotten). - Replicates the fix's exact resume steps directly via WashDataManager
(no adapter-core dependency) and confirms the orphaned cycle
actually reaches "off" and appears in cycle history, instead of
staying stuck. - A source-inspection check that main.js's branch still seeds
lastAboveThreshold and calls processPowerReading() (verified this
fails as expected when the branch is removed entirely).
Verified with a real js-controller integration test run (test/integration.js)
that the adapter still starts up cleanly after this onReady() change.
- main.js: new restore branch for "cycle was running, power now idle"
- tests/test_restart_resume_low_power.js: new regression test (3 cases)
- package.json / io-package.json: 0.4.22 -> 0.4.23
- io-package.json: news entry (11 languages), trimmed oldest (0.4.16)
- README.md: changelog entry
Verified locally: eslint clean, npm test (71 passing, up from 68),
npm run test:package (59 passing), npm run test:integration (2 passing,
real js-controller startup), JSON validation on io-package.json.
Release v0.4.22
Real bug found via a production power trace: a dryer cycle stayed
stuck as "running" long after power had genuinely dropped to ~0W,
with no end ever detected ("kein Ende erkannt").
Root cause: CycleDetector's constructor merges its config with a plain
object spread ({ ...DEFAULT_CONFIG, ...config }). The admin UI's
per-device "Off delay" setting is stored as offDelayMin (minutes) on
the normalized device config passed in from main.js/washDataManager.js,
but the detector's own field for this is offDelay (seconds) - two
different key names for the same setting that a plain spread merge
never reconciles. So every device silently used
DEFAULT_CONFIG.offDelay (a hardcoded 300s/5min), completely ignoring
whatever offDelayMin was actually configured (including the
device-type-based 5/8/10 min defaults main.js computes for it).
Verified this is the only field affected: cross-checked every key
CycleDetector's DEFAULT_CONFIG defines against what main.js's
normalized device config actually provides. All the other unmatched
keys (endEnergyThreshold, startOffDelay, minOffGap, suspiciousWindow,
pauseDelay, endPhaseOffDelay, endPhasePowerRatio, endPhaseProgressMin,
watchdogMs, zombieFactor, traceResolutionMs) are intentionally
internal tuning constants never exposed in the admin UI - offDelayMin
is the only one that IS a real admin setting and was still silently
broken.
Fix: CycleDetector's constructor now explicitly translates
config.offDelayMin (minutes) into this.cfg.offDelay (seconds) after
the spread merge, when provided.
Important caveat, called out in the changelog too: this fix alone
does not fully explain a stall of well over an hour (even the wrong
hardcoded 5-minute default should have ended the cycle far sooner) -
there may be an additional, not-yet-identified cause for very long
stalls. Documented as an open question rather than overclaiming a
complete fix.
Added tests/test_offdelay_wiring.js: verifies offDelayMin gets
translated to the right offDelay value, that the default is preserved
when offDelayMin is absent, and an end-to-end check that a cycle
actually ends around the configured offDelayMin rather than the
hardcoded default (verified this fails as expected with the bug
present and passes with the fix).
- lib/cycleDetector.js: offDelayMin -> offDelay translation in the constructor
- tests/test_offdelay_wiring.js: new regression test (3 cases)
- package.json / io-package.json: 0.4.21 -> 0.4.22
- io-package.json: news entry (11 languages), trimmed oldest (0.4.15)
- README.md: changelog entry
Verified locally: eslint clean, npm test (68 passing, up from 65),
npm run test:package (59 passing), JSON validation on io-package.json.
Release v0.4.21
Addresses the manual review round covering admin/tab_m.html, which the
existing English-only regression test never checked (it only covered
main.js and lib/*.js).
German strings in admin/tab_m.html
Found more than the ~33 the review listed (its own line list missed
at least two: "Zyklus wirklich beenden?" / "Zyklus wird beendet..." at
lines 1521/1524) by doing a systematic dictionary sweep of the whole
file rather than only patching the exact reported lines. All wrapped
in the existing _() translation function; 24 new i18n keys added
across all 11 languages (191 -> 215 keys, verified consistent).
One deliberate exception left alone: LEGACY_DEFAULT_MSGS (the old
hardcoded German default message texts, used only for a string
comparison to detect un-customized legacy config - never rendered to
a user). The new regression test excludes this block by content match,
not a line number, so it won't silently stop checking if the file
changes later.
matchedProfile stored as literal German text (found while fixing the above)
cycle.matchedProfile was set to the literal string "🌀 Anti-Knitter"
in main.js for cycles tagged via the anti-crease reference pattern.
This value gets written directly to the lastCycleProgram data point
(main.js) and rendered as-is in four places in the admin tab's cycle
history tables - meaning it showed up as German text in a data point
and in the UI regardless of the system's configured language, not
just as an isolated UI-label problem.
Fixed properly rather than just translating it in place: main.js now
stores a plain, language-neutral marker ("Anti-crease") instead, and
a new getProfileDisplayName() helper in tab_m.html translates it (with
its emoji) at display time - the same pattern already used for
phase/state keys. The isAntiKnitter comparison in tab_m.html was
updated to match the new marker.
Hardcoded 'de-DE' locale (2 instances)
admin/tab_m.html:1062 (program creation date in the Programs table)
and the fmt() helper used for graph trim/split timestamps both used
'de-DE' instead of the adapter's _currentLang variable, unlike every
other date/time formatting call in the file. Both now use
_currentLang || 'en'.
Full MIT license text in README.md
Checklist V2.2 requires the complete license text in README.md, not
just a copyright header. Copied verbatim from LICENSE so both stay in
sync.
Tests
Extended tests/test_english_only.js with three new checks specific to
admin/tab_m.html: no known-German UI strings remain (verified each
catches its specific regression), no hardcoded per-locale
toLocale*String() calls, and no _() key used in tab_m.html is missing
from admin/i18n/en.json (verified by deliberately introducing an
undefined key and confirming the test fails).
- main.js: matchedProfile marker changed to "Anti-crease"
- lib/washDataManager.js: translated one remaining German JSDoc comment
- admin/tab_m.html: all German UI strings translated, both de-DE
instances fixed, new getProfileDisplayName() helper - admin/i18n/*.json: 24 new keys across all 11 languages (191 -> 215)
- README.md: full MIT license text, changelog entry
- tests/test_english_only.js: three new tests covering admin/tab_m.html
- package.json / io-package.json: 0.4.20 -> 0.4.21
- io-package.json: news entry (11 languages), trimmed oldest (0.4.14)
Verified locally: npm run lint, npm test (65 passing, up from 62),
npm run test:package (59 passing), JSON validation on io-package.json,
jsonConfig.json, and all 11 i18n files (215 keys each, consistent).
Release v0.4.20
[W3026] adapter-tests matrix was missing Node.js 26. Added 26.x to
both unit-tests and adapter-tests matrices for consistency.
[S0064] @iobroker/testing 5.3.0 -> 6.1.0. Checked both 6.0.0's and
6.1.0's breaking changes against this adapter before bumping:
- 6.0.0: 'fail if a prepare script is defined in package.json' - we
don't have one. 'Dropped support for Node.js 18/20' - we already
require >=22. - 6.1.0: 'changeAdapterConfig() now auto-encrypts encryptedNative
properties' - we don't define any encryptedNative properties in
io-package.json, and test/integration.js never calls
changeAdapterConfig() at all (it only calls startAdapterAndWait()
with the default zero-device config).
Neither breaking change applies. Verified with a full fresh install:
npm run lint, npm test (62 passing), npm run test:package (59 passing,
now also validates the new admin/phaseLabels.json automatically), and
npm run test:integration (2 passing, full real js-controller startup).
- package.json: @iobroker/testing ^5.3.0 -> ^6.1.0, version 0.4.19 -> 0.4.20
- .github/workflows/test-and-release.yml: added 26.x to both test matrices
- io-package.json: news entry (11 languages), trimmed oldest (0.4.13)
- README.md: changelog entry
Release v0.4.19
Repository checker findings from the latest recheck:
[E5043] fs/path built-in modules must use the node: prefix. main.js
already did this correctly; lib/displayLabels.js, test/integration.js,
and test/package.js still used the bare require("fs")/require("path")
form. All four now use node:fs/node:path consistently.
[W0034] @iobroker/adapter-core 3.4.1 was specified; 3.4.3 is current.
Bumped package.json's dependency accordingly.
[W1145] The Dutch/Polish/Ukrainian common.news translations for 0.4.12
were noticeably shorter than the English original (471/490/466 chars
vs 855) - they were missing roughly the second half of the entry
(the multi-device config-combining fix). Replaced with full-length
translations matching the other languages' content for that entry.
Checked all other news entries for the same class of gap: found lower
character-count ratios in several zh-cn entries and a few 0.4.17
entries too, but those are expected (zh-cn is inherently far more
compact per character - not missing content) or not actually flagged
by the checker (0.4.17's ratios are all higher than 0.4.12's flagged
ones), so left alone rather than over-correcting based on a rough
heuristic that isn't calibrated to the checker's actual threshold.
[W4001] unchanged - expected until the "add to latest" PR merges.
- package.json: @iobroker/adapter-core ^3.4.1 -> ^3.4.3, version 0.4.18 -> 0.4.19
- io-package.json: news entry (11 languages), trimmed oldest (0.4.12,
now fully superseded - its content remains in README.md's Changelog) - README.md: changelog entry
Verified locally: npm run lint, npm test (62 passing), npm run
test:package (58 passing), JSON validation on io-package.json.
Release v0.4.18
Real bug found via production logs: the dryer's anti-crease lock
period (set after the beep to prevent post-cycle tumbling from being
mistaken for a new cycle) had a fixed length - the learned reference
pattern's duration plus a flat 10-minute buffer. Real anti-crease
duration varies cycle to cycle (load size, dampness, etc.), so on this
occasion the fixed lock expired at 23:47:31 while the dryer was still
genuinely tumbling, and the very next tumble spike (23:47:38) got
picked up as a real new cycle starting.
Fix: the lock is now adaptive. Any reading during the lock period that
is a genuine anti-crease tumble spike (above idle standby noise
10W, but below the 'real new cycle' threshold) pushes the lock's
expiry forward by ANTI_CREASE_LOCK_REFRESH_MS (10 min), capped at
ANTI_CREASE_LOCK_MAX_MS (90 min total) from when the lock was first
set (_dryerLockStartedAt) - so genuinely ongoing anti-crease activity
keeps the protection alive, while a pathological case (e.g. a stuck
sensor) still can't lock forever.
The existing 'sustained high power for 30s -> real new cycle, lock
lifted immediately' detection is untouched and takes priority - the
extension only applies to sub-threshold tumble spikes, not genuine new
cycles.
Added tests/test_anticrease_lock_extension.js: verifies the lock
extends when a tumble spike occurs near expiry (bounded by the 90min
ceiling), that it still eventually expires once nothing pushes it
forward, and that sustained genuinely high power still correctly lifts
the lock as a real new cycle regardless of the extension logic.
- lib/washDataManager.js: two new module constants
(ANTI_CREASE_LOCK_REFRESH_MS, ANTI_CREASE_LOCK_MAX_MS), new
_dryerLockStartedAt field (reset everywhere _dryerLockUntil is
nulled), extension logic in the anti-crease spike-ignored branch - package.json / io-package.json: 0.4.17 -> 0.4.18
- io-package.json: news entry (11 languages), trimmed oldest (0.4.11)
- README.md: changelog entry
Verified locally: npm run lint, npm test (62 passing, up from 60),
npm run test:package (58 passing).
Release v0.4.17
New feature: three new read-only data points per device, alongside the
existing language-neutral ones:
- phaseText - localized, emoji-prefixed version of
phase
(e.g. "washing" -> "🫧 Washing" / "🫧 Wäscht") - stateText - localized version of
state
(e.g. "running" -> "Running ⚙️" / "Läuft ⚙️") - programText - localized version of
program; a confirmed program
is the user's own saved profile name and is passed through
unchanged (not our vocabulary to translate) - only the
"detecting..." sentinel gets localized
Motivation: phase/state/program are intentionally language-neutral
internal keys (meant for automations/scripts, stable across ioBroker
system-language changes), which means anyone wanting to show status
directly in a VIS dashboard had to build their own translation table
first. The new *Text data points solve that directly.
New per-device admin setting "Display language" (defaults to "" =
system language, or pick one of the 11 supported languages
explicitly) - resolved once per device at adapter startup into
this._deviceLangCache (not per status update, since it can't change
without an instance restart anyway).
Architecture: the phase-translation table (12 phases x emoji + 11
languages) previously existed only client-side, hardcoded in
admin/tab_m.html for the cycle-graph legend (PHASE_LABELS). Extracted
it into a new shared admin/phaseLabels.json, following the exact same
pattern already used for admin/i18n/.json: the admin tab now
fetches it (loadPhaseLabels()), and the new lib/displayLabels.js reads
it via fs.readFileSync() - one source of truth instead of two
separately maintained copies that could drift apart (exactly the risk
class already addressed for other translation tables in this project).
lib/displayLabels.js also reuses the state-label wording already
established in admin/i18n/*.json (the "Off"/"Starting…"/"Running
⚙️"/"Paused"/"Ending…" keys already shown in the admin tab's device
list) rather than inventing a second wording for the same five words.
Added a new "detecting..." i18n key (previously only appeared inside
a longer help-text sentence, not as its own translatable key).
onReady() now calls loadNotifTranslations(this) early (previously only
lazily on the first notification ever sent), so the system-language
dictionary is available immediately for phaseText/stateText/programText,
not just for notifications.
Also found and fixed one more leftover German comment ("Datenpunkte
direkt setzen") in the override-restore-on-startup code path while
wiring up the new data points there - same sweep, different spot.
Translated remaining German code comments to English
Not a checker/review requirement (developer-only comments in German
are explicitly permitted per the review rules) - done on request.
Multiple widening dictionary/keyword sweeps across main.js, lib/*.js,
and admin/tab_m.html.
Reorganized the README changelog
Found a real inconsistency: version 0.2.5 existed with near-duplicate
but not identical text in both README.md's main Changelog section and
CHANGELOG_OLD.md. README.md's Changelog now covers the current 0.4.x
series in full; CHANGELOG_OLD.md now holds the complete pre-0.4.0 beta
history (0.2.2-0.3.0), each version in exactly one place.
Tests
Added tests/test_display_labels.js: getPhaseText()/getStateText()/
getProgramText() - known keys, unknown keys fall back gracefully,
empty input, language fallback to English, and that the profile-name
pass-through in getProgramText() really does nothing to the user's
own text. Extended tests/test_english_only.js's migration-completeness
check with an allow-list for brand-new states that never had a wrong
legacy name to migrate.
- package.json / io-package.json: 0.4.16 -> 0.4.17
- io-package.json: news entry (11 languages), trimmed oldest (0.4.10)
- README.md: changelog entry, data-points table, Features bullet
Verified locally: npm run lint, npm test (60 passing, up from 44),
npm run test:package (58 passing), JSON validation on io-package.json,
jsonConfig.json, phaseLabels.json, and all 11 i18n files (191 keys
each, consistent).
Release v0.4.16
New feature: notification message templates (start/update/done, for
Telegram/Pushover/Signal/WhatsApp/Matrix/email/etc.) now support a
{state:objectId} placeholder alongside the existing built-in ones
({device}, {program}, {progress}, ...). At send time it resolves the
current value of any ioBroker data point via getForeignStateAsync() -
e.g. {state:0_userdata.0.outsideTemp} to include the outside
temperature, or a live electricity price, in a message.
Works with the existing conditional-block syntax ([...]) for free:
{state:objectId} placeholders are rewritten to plain {varN}
placeholders with their resolved value merged into the same vars map
the built-in placeholders use, before the existing block-hiding logic
runs - so a block containing a {state:...} placeholder is hidden
exactly like it would be for an empty/0 built-in variable, with no
special-casing needed.
A missing or unreadable object ID resolves to an empty string rather
than throwing, so one bad reference in a template can't break the
whole notification (in an empty [...] block it just hides that block;
used bare, the placeholder resolves to nothing).
Implementation: extracted as a plain function into a new
lib/notifyTemplate.js (resolveStatePlaceholders), rather than a method
on the adapter class in main.js. This has no dependency on
@iobroker/adapter-core, matching how the other lib/ modules take a
plain adapter-shaped object rather than extending the real Adapter
base class - and, concretely, requiring main.js directly for a unit
test fails in this environment ("Cannot find js-controller") because
main.js requires adapter-core at module load time, which the other
lib/ files deliberately avoid.
Found and fixed in passing: the notification subject line still had
one leftover German word ("Fertig" for the "done" event) that the
English-only sweep in 0.4.12 missed.
UI/docs: added {state:objectId} (and the two previously undocumented
{progress}/{prevTime}) to the "Placeholders:" hint in the Notifications
tab. README gets a new "Using data points in notification messages"
section with a worked example, plus a Features bullet mention.
Added tests/test_notification_state_placeholders.js: resolution with a
real value, missing/erroring state resolves to empty without throwing,
duplicate {state:ID} in one template only fetches once, and a template
with no {state:...} at all is returned completely unchanged.
Translated remaining German code comments to English
Not a checker/review requirement (developer-only comments in German
are explicitly permitted per the review rules) - done on request.
Found via several widening dictionary/keyword sweeps across
main.js, lib/*.js, and admin/tab_m.html: section-header comments,
inline explanations, and a few JSDoc @PARAM descriptions. Left
untouched: two places that quote the old German state name
("Läuft") as a historical example in prose, which are correct as-is.
Reorganized the README changelog
Found a real inconsistency while at it: version 0.2.5 existed with
near-duplicate but not identical text in both README.md's main
Changelog section and CHANGELOG_OLD.md (a leftover from
@alcalzone/release-script auto-archiving io-package.json's news list,
which is capped at 7 entries by the repository checker - unlike
README's own Changelog section, which has no checker-enforced length
limit and was never pruned to match).
Reorganized so each version's changelog exists in exactly one place:
README.md's Changelog section now covers the current 0.4.x series
(0.4.0 through 0.4.16, in full), and CHANGELOG_OLD.md now holds the
complete pre-0.4.0 beta history (0.2.2 through 0.3.0) that used to
also be duplicated in README.
- package.json / io-package.json: 0.4.15 -> 0.4.16 (unchanged from the
prior local commit - nothing had been pushed yet, so this round of
changes was folded into the same version rather than bumping again) - io-package.json: news entry (11 languages), trimmed oldest (0.4.9)
- README.md: changelog entry, changelog reorganization, new docs section
- CHANGELOG_OLD.md: full pre-0.4.0 history, deduplicated against README
Verified locally: npm run lint, npm test (44 passing), npm run
test:package (57 passing), JSON validation on io-package.json,
jsonConfig.json, and all 11 i18n files (186 keys each, consistent).
Release v0.4.15
Compact-mode isolation: _notifTranslations/_notifLang were module-level
let variables. io-package.json declares compact:true; two instances
of this adapter sharing one process would share this cache, leaking
one instance's detected system language into the other's notification
templates. Moved to adapter._notifTranslations/_notifLang. Same bug
class already fixed for ProfileStore.MIN_CONFIDENCE previously.
Multi-device: sensorToDevice[powerId] = deviceId was an unguarded
assignment. Two devices (legacy + array, or two array entries)
accidentally sharing one power sensor would silently overwrite the
mapping - the first device would stop receiving power readings with
no warning anywhere. Added a log.warn() when this happens.
Config normalization: durationTolerance/matchIntervalMin/matchPersist/
offDelayMin used a bare || default, the same falsy-zero-swallowing
pattern already found and fixed for powerThreshold/startEnergyThreshold
in 0.2.3 (per this project's own changelog) but not applied
consistently to every numeric field. Not reachable via the admin UI's
min bounds today, but inconsistent and fragile against manually-edited
native config. Aligned to the strict undefined/null/empty-string check.
Removed dead code: msgBlockTimer was checked/cleared in
_onCycleFinished() but never assigned anywhere - vestigial from a
removed feature.
Added tests/test_review_findings.js covering the two behavioral
findings (module-level cache, unguarded duplicate powerId). Verified
both fail as expected against the pre-fix code and pass with the fix.
- package.json / io-package.json: 0.4.14 -> 0.4.15
- io-package.json: news entry (11 languages), trimmed oldest (0.4.8)
- README.md: changelog entry
Verified locally: npm run lint, npm test (40 passing, up from 38),
JSON validation on io-package.json.
Release v0.4.14
admin/icon.png was only 128x128 (the bare minimum per ioBroker's
adapter dev guide, which recommends 256x256). Replaced with a new,
sharper 256x256 icon, resized from a 1254x1254 source down to the
recommended size (no need to ship a multi-megapixel PNG for a 256px
icon).
Only reference to the icon file is admin/icon.png itself (io-package.json's
icon/extIcon fields and README.md's logo badge all just point at it,
nothing else to update).
- package.json / io-package.json: 0.4.13 -> 0.4.14
- io-package.json: news entry (11 languages), trimmed oldest entry
(0.4.7) to stay within the 7-entry limit - README.md: changelog entry
Verified locally: npm run lint, npm test (38 passing), JSON validation
on io-package.json.