Skip to content

feat: dispatch on the panel's real data-model-version - #155

Merged
cayossarian merged 2 commits into
developfrom
feat/live-schema-dispatch
Aug 4, 2026
Merged

feat: dispatch on the panel's real data-model-version#155
cayossarian merged 2 commits into
developfrom
feat/live-schema-dispatch

Conversation

@cayossarian

Copy link
Copy Markdown
Member

Phase 2, Part A — the half that does not depend on the §5 integration fork.

The bug

create_span_client hardcoded data_model_version = None, so the guard that
refuses a parent/child panel was written, tested, and never invoked. Every
panel resolved to the flat parser regardless of what it reported.

It did not even fail cleanly. Reverting the dispatch in a test shows what a
1.0 panel actually got:

ValueError: Schema missing 'energy.ebus.device.circuit/space' property

The flat parser reaches for a type that parent/child firmware keeps under
deviceClasses — so the user sees a complaint about a missing property, for a
panel whose real problem is that nothing installed can parse it.

The fix

The Homie schema is fetched over REST before the broker is opened, and its
dataModelVersion selects the adapter. SPAN confirmed this is supported:

real flat firmware serves GET /api/v2/homie/schema and omits
dataModelVersion, so the absence of that field over REST is a reliable
flat-versus-parent/child discriminator … You can dispatch on REST before
opening MQTT.

A 1.0 panel now raises SpanPanelAdapterMissingError naming the adapter to
install and what is already present.

Dispatch also moved to wherever a parser is built, not just the factory
path. A directly constructed SpanMqttClient — which the README documents and
the integration uses — previously always resolved the flat adapter, carrying
the identical defect. There is now one answer rather than two.

Protocol reshape — breaking, and deliberately all at once

This is the release that breaks the protocol, so it changes shape once:

Before After Why
__init__(serial_number, panel_size) __init__(serial_number, schema) panel_size is read from a block only the flat schema has. The bootstrap had to understand a wire format it is meant to know nothing about, and a differently-shaped adapter had no way to say so.
build_field_metadata(schema_types) build_field_metadata() Same flat-shaped parameter on a format-agnostic protocol. The adapter holds its own schema.

Tier 1 dispatch moved from the private factory._select_adapter_key to
span_panel_api.dispatch.select_adapter_key, so the transport can dispatch
without importing the factory. adapters.py still answers what is installed;
dispatch.py answers what does this panel need.

Also pinned: the enum rule that runs the other way

v1.0 requires consumers not to raise on an unrecognised value in a
$format-extended enum — the opposite of the data-model-version rule living
one import away. The difference is blast radius: an unknown enum member affects
one property, an unknown schema version means the whole tree may be misread.
Both are now tested, because schema_1 inherits the enum one and the two are
easy to confuse.

Verification

  • 438 tests (+6), coverage 94%, all hooks green.
  • Mutation-checked: reverting dispatch to select_adapter_key(None) fails
    test_a_parent_child_panel_is_refused_rather_than_parsed_as_flat with the
    exact ValueError quoted above.
  • New tests cover dataModelVersion parsing (absent / present / unquoted), the
    factory refusing a v1.0 panel, direct construction dispatching, diagnostics
    being filled in by dispatch, and enum pass-through.

Not in this PR

schema_1 itself — gated on the §5 fork, which the design doc's
"Re-checked 2026-08-04" section now scopes.

The guard that refuses a parent/child panel was written, tested, and never
invoked: create_span_client hardcoded data_model_version = None, so every
panel resolved to the flat parser no matter what it reported. A v1.0 panel
did not fail cleanly either — the flat parser reached for
energy.ebus.device.circuit/space, which parent/child firmware keeps under
deviceClasses, and the run died on "Schema missing
'energy.ebus.device.circuit/space' property": a complaint about a missing
property, for a panel whose actual problem is that nothing installed can
parse it.

The Homie schema is now fetched over REST before the broker is opened, and
its dataModelVersion selects the adapter. SPAN confirmed the absence of that
field on this endpoint is a reliable flat-versus-parent/child signal,
mirroring MQTT's info/data-model-version, and that dispatching on it before
opening MQTT is supported. A 1.0 panel now raises
SpanPanelAdapterMissingError naming the adapter to install.

Dispatch also moved to wherever a parser is built, not just the factory path.
A directly constructed SpanMqttClient — which the README documents and the
integration uses — previously always resolved the flat adapter, carrying the
same defect the factory had.

The protocol changes shape once, here, because this is the release that
breaks it:

- __init__ takes the schema rather than panel_size. Deriving panel_size means
  reading a block only the flat schema has, so the bootstrap had to
  understand a wire format it is meant to know nothing about, and an adapter
  shaped differently had no way to say so.
- build_field_metadata() takes no arguments; the adapter holds its schema.

Tier 1 dispatch moved to span_panel_api.dispatch so the transport can reach
it without importing the factory. adapters.py still answers "what is
installed"; dispatch.py answers "what does this panel need".

Also pins the enum-tolerance rule that schema_1 inherits: v1.0 requires
consumers not to raise on an unrecognised value in a $format-extended enum,
which is the opposite of the version rule one import away. The difference is
blast radius — an unknown enum member affects one property, an unknown schema
version means the whole tree may be misread.

438 tests pass, coverage 94%.
The check builds a parser against a real bootstrap-only wheel, so it still
passed a panel size and died on AttributeError before reaching the error it
exists to assert. The unit suite could not catch this: it never runs against
an install that has no adapter.

It now passes a flat schema, which is also the case this check is about —
every panel in the field reports no data-model-version, so dispatch asks for
the default key and finds nothing providing it.
@cayossarian
cayossarian merged commit 75f2ca9 into develop Aug 4, 2026
5 checks passed
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.

1 participant