XKNX / XKNX/xknxtoolkit

[Detail Bug] Catalog: Multi-SKU KNX hardware shows wrong order number/width in Project Configure panel

Open
#90 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_f0b1cba1-67f9-40dd-932f-b0ff832f3233

Introduced in #28 by @kewde on Sep 7, 2026

Summary

  • Context: upload_knxprod ingests a .knxprod into the catalog DB. In _ingest_hardware, each hardware row's product-level fields (name, order_number, is_rail_mounted, width_mm, description, default_language) are taken from one Product — the first element of reg.products_for_hardware(hardware_id).
  • Bug: A KNX <Hardware> is permitted by schema and parser to have multiple <Product> children (the same device under several SKUs / order numbers). _ingest_hardware collapses that list to products[0] and silently discards every other SKU's payload.
  • Actual vs. expected: expected — every product/SKU attached to a hardware is represented (or, at minimum, the collision is surfaced, the way this codebase handles "data that can't be fully represented" elsewhere). Actual — non-first SKUs' display attributes vanish without a warning, and the single Hardware row carries a SKU that may not be the one a catalog entry sells.
  • Impact: For any multi-SKU hardware whose <Hardware> carries N>1 <Product> and a <CatalogItem> references a non-first product, list_products would return a ProductSummary row whose product_ref_id and order_number come from different products. More concretely, the project configure panel renders order_number, is_rail_mounted and width_mm for an added device unconditionally from the catalog Hardware row, so such a catalog item's entry would surface the wrong order_number and potentially wrong width_mm for the device the operator added.

Code with Bug

packages/catalog/src/xknxmono/catalog/core/upload.py:

products = list(reg.products_for_hardware(hardware_id).values())
product = products[0] if products else None   # <-- BUG 🔴 silently keeps only the first SKU; products[1:] are dropped
session.merge(
    Hardware(
        id=hw.id,
        manufacturer_id=mfr_id,
        name=product.name if product else hw.name,
        order_number=product.order_number if product else None,
        is_rail_mounted=product.rail_mounted if product else None,
        width_mm=product.width_mm if product else None,
        description=product.raw.visible_description if product else None,
        default_language=product.raw.default_language if product else None,
        ...
    )
)

packages/catalog/src/xknxmono/catalog/core/upload.py (catalog item can reference a different SKU than the one ingested into Hardware):

session.merge(
    CatalogSectionProduct(
        id=item.id,
        ...
        product_ref_id=item.product_ref_id,   # <-- BUG 🔴 can reference a non-first SKU whose display payload was discarded
        ...
    )
)

Explanation

  • The KNX schema and parser allow multiple <Product> elements per <Hardware> (registry stores hardware_to_product as a list, and the parser collects all products with no len == 1 guard). The catalog DB model (Hardware) has only a single set of product-level columns, so _ingest_hardware resolves the mismatch by selecting products[0] and discarding the rest.
  • If a CatalogItem references a non-first SKU via CatalogSectionProduct.product_ref_id, the DB now contains inconsistent information: the SKU reference points to (e.g.) “Beta”, but Hardware.order_number/width_mm reflect “Alpha”.
  • This inconsistency is surfaced in two places:
    • list_products returns a ProductSummary that pairs product_ref_id from CatalogSectionProduct with display fields (like order_number) sourced via the Hardware row, so a single row can describe two different SKUs.
    • The Project Configure panel resolves hardware by program ref and renders hardware.order_number/hardware.width_mm/hardware.is_rail_mounted unconditionally, so a device added from a non-first SKU can display the wrong order number (and possibly width) with no ingest-time warning.

Codebase Inconsistency

  • The ingestion path persists CatalogSectionProduct.product_ref_id (SKU-specific) but populates Hardware.order_number (and other display fields) from products[0] without reconciling it against product_ref_id. This allows product_ref_id and order_number to refer to different SKUs in the same returned/rendered entity.

Recommended Fix

  • Detect and surface the unrepresentable case at ingest time: in _ingest_hardware, warn or raise when len(products) > 1 instead of silently dropping SKUs.
  • To fully fix the operator-facing mismatch, ensure per-SKU display fields (order_number, width_mm, etc.) are sourced from the SKU referenced by product_ref_id (e.g., persist a per-SKU table keyed by Product.id and join on CatalogSectionProduct.product_ref_id, or otherwise resolve product metadata by product_ref_id when building ProductSummary/hardware info).

History

This bug was introduced in commit ab3210b. The original products[0] silent-drop in _ingest_hardware (first written in dcc7b8c, May 2026) was for its whole first quarter only a latent data-loss with no operator-visible surface, but commit ab3210b "feat(knx-gui): rename Manufacturer section to Metadata, add full device metadata (#28)" added both xknxmono.catalog.get_hardware_by_program (the catalog read that fetches the stale Hardware row) and the project-side CONFIGURE_ORDER_NUMBER/CONFIGURE_RAIL_MOUNTED/CONFIGURE_WIDTH rendering that consumes hardware.* unconditionally — turning the silent ingest-time drop into an operator-facing wrong-render exactly on the surface this report identifies (metadata_section.py:79,95,101). The escalating commit is therefore ab3210b; the deeper latent mechanism traces back to dcc7b8c's original _ingest_hardware, and c75ed30's ProductSummary (June 2026) was an intermediate escalation that paired product_ref_id with Hardware.order_number in a single row.

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/catalog/src/xknxmono/catalog/core/upload.py at _ingest_hardware and trace products_for_hardware through CatalogSectionProduct.product_ref_id. Then inspect metadata_section.py at the configure metadata rendering mentioned in the report. Done means multi-SKU hardware no longer produces mismatched product references and display fields, with the chosen ingest or per-SKU handling documented and covered by relevant checks.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.