Skip to content

Detect changes by value, not by the controller's lc (#295) - #575

Merged
GermanBluefox merged 1 commit into
masterfrom
fix/alias-changes-only
Oct 3, 2026
Merged

GermanBluefox merged 1 commit into
masterfrom
fix/alias-changes-only

Conversation

@GermanBluefox

Copy link
Copy Markdown
Contributor

Fixes #295. "Record changes only" stored a row for every source change of an alias, even when the read converter mapped them all to the same value — the reporter saw hundreds of identical rows, and two other users confirmed it.

Cause

pushHistory() decided whether a value had changed from state.ts !== state.lc:

if ((this.sqlDPs[id].state.val !== null || state.val === null) && state.ts !== state.lc) {

That is right for an ordinary state — js-controller only moves lc when the value really changes. It is wrong for an alias: an alias has no value of its own, so it carries the lc of its source. A source going from 12.34 to 12.31 moves lc, and the alias arrives with ts === lc although Math.round(val*10)/10 maps both to 12.3.

This is what @Apollon77 described in 2024 — "fixing this requires that the adapter builds up his own last value cache and do the comparison based on the real value". That cache already exists: sqlDPs[id].state is assigned only on the logging path, right before pushHelper(), so it holds the value that was last written — exactly the reference changesOnly needs. The neighbouring changesMinDelta check already compared values this way.

Scope

Three of the four occurrences are replaced: the changesOnly branch with and without a relog interval, and the relog trigger.

The fourth, in the debounce branch, is deliberately left alone. There the correct reference is the value the running timer is about to store, not the last stored one — and the adapter does not track it separately. Comparing against the stored value would make a 5 → 7 → 5 excursion inside the debounce window look unchanged and keep the timer from restarting, which would be a new bug rather than a fix.

Two cases that are easy to get wrong

isSameValue() lives in src/lib/values.ts so it is unit testable without importing main.ts, and so both land in one place:

  • Objects are compared by JSON, because ts_string stores them JSON.stringifyed. A strict !== compares references, would call every update a change, and would defeat changesOnly for exactly those datapoints — the opposite of this fix.
  • Two NaN readings count as unchanged, since NaN === NaN is false but a sensor repeating NaN has not changed.

A string and a number that look alike ('12.3' vs 12.3) stay a change: they go into different tables, so collapsing them would lose a real transition.

Verification

test/testValues.js, 8 cases, in the CI unit list — including the issue's own converter. 192 unit tests pass, npm run check:ts, npm run lint (both passes) and prettier --check are clean.

Because this touches the write path, it was also run against the full SQLite integration suite locally: 36 passing, 0 failing. The changed branch is genuinely exercised there — value not changed appears 6× in the debug log (the changesOnly branch skipping correctly) and value not changed debounce 2× (the branch left untouched).

🤖 Generated with Claude Code

"Record changes only" decided whether a value had changed from
`state.ts !== state.lc`. That is right for an ordinary state - js-controller
only moves `lc` when the value really changes - but wrong for an alias. An
alias has no value of its own, so it carries the `lc` of its source: a source
going from 12.34 to 12.31 moves `lc`, and the alias arrives with ts === lc
although its read converter maps both to 12.3. Every source change was stored,
which is what the reporter saw as hundreds of identical rows.

The comparison now runs against `sqlDPs[id].state.val`, which is assigned only
on the logging path and therefore holds the value that was last written -
exactly the reference "changes only" needs. The neighbouring changesMinDelta
check already compared values this way.

isSameValue() lives in src/lib/values.ts so it can be unit tested without
importing main.ts, and so that the two cases that are easy to get wrong are in
one place: objects are compared by their JSON, because ts_string stores them
JSON.stringify()ed and a strict !== would call every update a change and defeat
changesOnly for those datapoints; two NaN readings count as unchanged.

Three of the four occurrences are replaced - the changesOnly branch with and
without a relog interval, and the relog trigger. The fourth, in the debounce
branch, is deliberately left alone: there the right reference is the value the
running timer is about to store, not the last stored one, and the adapter does
not track it separately. Comparing against the stored value would make a
5 -> 7 -> 5 excursion inside the debounce window look unchanged and keep the
timer from restarting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@GermanBluefox
GermanBluefox merged commit eed7287 into master Oct 3, 2026
17 checks passed
@GermanBluefox
GermanBluefox deleted the fix/alias-changes-only branch October 3, 2026 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQL driver: wrong detection of changes for aliase driver

1 participant