XKNX / XKNX/xknxtoolkit

[Detail Bug] Dynamic UI: Inline parameter calculations write literal "{}" instead of clearing to defaults

Open Beginner friendly
#94 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
4
Forks
0
Avg merge
15h 38m
Merged PRs (30d)
37

Description

Detail Bug Report

https://app.detail.dev/org_62aa40f5-2c23-4914-a665-3bb2068af20e/bugs/bug_8bac50a9-da14-45e5-849b-3c3077c2226b

Introduced in debd9ef940657cc09e589acd992c251373d9b286 by @kewde on Jun 18, 2026

Summary

  • Context: evaluate_lr/evaluate_rl in packages/product/src/xknxmono/product/parser_v2/calculation/__init__.py run a KNX product's JavaScript ParameterCalculation transformations (via dukpy) to derive one set of parameter values from another; DynamicUI.set_parameter_ref uses the returned dict to set or clear the derived parameters.
  • Bug: The inline transformation branch returns the literal string "{}" for every declared output the script left unset, instead of returning None (and thus being excluded from the returned dict). Because the caller only clears a parameter when the returned value is None, the "{}" string is written verbatim into parameter state.
  • Actual vs. expected: Actual: unset inline-calc outputs become "{}" and are persisted as overrides (and may propagate into encoded device memory). Expected: when a calculation produces no value for a declared output, the parameter is cleared so it reverts to its static default.
  • Impact: KNX parameter state is silently corrupted with the literal string "{}" instead of reverting to static defaults; on the gira_2gang_button_interface.knxprod fixture, an API call sequence can produce an encode_to_memory() image that differs by two bytes compared to the contract-intended clear-to-default behavior.

Code with Bug

In packages/product/src/xknxmono/product/parser_v2/calculation/_js.py:

def _read_js_var(interp: dukpy.JSInterpreter, expr: str) -> str | None:
    try:
        v = interp.evaljs(expr)
    except Exception:
        return None
    if v is None:
        return None
    if isinstance(v, (int, float)):
        fv = float(v)
        return str(int(fv)) if fv == int(fv) else str(fv)
    return str(v)  # <-- BUG 🔴 dukpy marshals JS `undefined` to {}, which becomes literal "{}"

def eval_inline(code, inputs, output_names) -> dict[str, str]:
    interp = dukpy.JSInterpreter()
    for name, val in inputs.items():
        interp.evaljs(f"var {name} = {_to_js_literal(val)};")
    for name in output_names:
        if name not in inputs:
            interp.evaljs(f"var {name};")  # <-- BUG 🔴 declares output as `undefined`, later read back as "{}"
    interp.evaljs(code)
    return {n: v for n in output_names if (v := _read_js_var(interp, n)) is not None}

In packages/product/src/xknxmono/product/parser_v2/dynamic.py:

for pr in calc.rparameters.parameter_ref_ref:
    v = r_values.get(pr.alias_name or pr.ref_id)
    if v is not None:
        self._state.set_instance_ref(pr.ref_id, v)  # <-- BUG 🔴 inline calcs pass "{}" (non-None), so it gets written
    else:
        self._state.clear_instance_ref(pr.ref_id)

Explanation

  • Under pinned dukpy==0.5.1, evaluating an unassigned JS variable (var y; y) marshals to a Python empty dict ({}), not None.
  • _read_js_var stringifies non-numeric, non-None return values via str(v), turning that {} into the string "{}".
  • eval_inline declares each output as var {name};, so any output the script doesn’t assign remains undefined and gets returned as "{}".
  • DynamicUI.set_parameter_ref only clears parameters when the returned value is None; since "{}" is non-None, it is persisted as a parameter override instead of clearing back to the static default.
  • The named-function evaluation path avoids this by initializing outputs to null (marshals to Python None), so unset outputs are excluded and the caller clears them.

Recommended Fix

Pre-initialize declared inline outputs to JS null so “unset” outputs marshal to None and are excluded from the returned dict (matching the named-function path):

for name in output_names:
    if name not in inputs:
        interp.evaljs(f"var {name} = null;")

Also update the existing test that currently asserts the (buggy) "{}" behavior for declared-but-unset inline outputs.

History

This bug was introduced in commit debd9ef9. That commit implemented inline calculation evaluation by declaring outputs as var {name}; (JS undefined), which dukpy marshals to {} and _read_js_var stringifies as "{}"; these values have been written into parameter state since inline evaluation was introduced.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/product/src/xknxmono/product/parser_v2/calculation/_js.py, focusing on eval_inline and its output initialization, then find the existing test for declared-but-unset inline outputs. Compare this path with the named-function evaluation path and verify that unset outputs are omitted so DynamicUI clears them; update the test to assert the clear-to-default behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, python
Domain
backend, testing-qa
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.