Skip to content

Add Gen3 gRPC transport layer#101

Merged
cayossarian merged 20 commits intomainfrom
grpc_addition
Feb 20, 2026
Merged

Add Gen3 gRPC transport layer#101
cayossarian merged 20 commits intomainfrom
grpc_addition

Conversation

@Griswoldlabs
Copy link

@Griswoldlabs Griswoldlabs commented Feb 19, 2026

Summary

Adds complete Gen3 gRPC transport support to the span-panel-api library, enabling communication with Gen3 Span panels (MAIN 40 / MLO 48) that use gRPC on port 50065 instead of REST.

What's included

  • grpc/ subpackageSpanGrpcClient with manual protobuf encoding (no .proto compilation needed), Subscribe RPC push-streaming, and automatic reconnection with 5s backoff
  • Protocol abstractionSpanPanelClientProtocol + capability mixins (AuthCapableProtocol, CircuitControlProtocol, StreamingCapableProtocol) for static type-safe dispatch across transports
  • PanelCapability flags — Runtime feature advertisement. Gen2: GEN2_FULL; Gen3: GEN3_INITIAL (read-only sensors + push streaming)
  • Unified snapshot modelSpanPanelSnapshot / SpanCircuitSnapshot returned by get_snapshot() on both transports
  • create_span_client() factory — Creates appropriate client by generation or auto-detects (probe Gen2 HTTP, fallback to Gen3 gRPC)
  • Circuit IID mapping — Authoritative Trait 15 (BreakerGroup) discovery with positional fallback. Fixes MLO 48 circuit mapping issues (contributed by @cecilkootz)
  • Dual-phase detection — Correctly identifies 240V circuits (dryer, range, etc.)
  • grpcio optional dependency — Install with span-panel-api[grpc]

Testing

Related

Changelog

See CHANGELOG.md entry for v1.1.15 (already included in this branch).

Test plan

  • Verify Gen2 REST path is completely untouched (no behavioral changes)
  • Test create_span_client() auto-detection with Gen2 panel
  • Test Gen3 gRPC connection + snapshot on MAIN 40 or MLO 48
  • Confirm poetry install with [grpc] extra pulls grpcio
  • Run existing unit tests to confirm no regressions

🤖 Generated with Claude Code

cayossarian and others added 11 commits February 17, 2026 12:19
…hot models

- Add PanelCapability flags and PanelGeneration enum to models.py
- Add SpanPanelSnapshot / SpanCircuitSnapshot unified data models
- Add SpanPanelClientProtocol + capability mixin Protocols (protocol.py)
- Add SpanGrpcClient Gen3 transport (grpc/client.py, grpc/models.py, grpc/const.py)
- Add create_span_client() factory with auto-detection (factory.py)
- Add SpanPanelGrpcError, SpanPanelGrpcConnectionError to exceptions.py
- Add get_snapshot() / connect() / ping() shims to SpanPanelClient for protocol conformance
- Add grpcio as optional [grpc] dependency; omit grpc/* and factory.py from coverage
- Add design document docs/Dev/grpc-transport-design.md
- Bump version to 1.1.15
…s fields

Add 17 optional Gen2-specific fields to SpanPanelSnapshot so that the HA
integration can derive all domain objects (SpanPanelHardwareStatus,
SpanPanelData, SpanPanelCircuit, SpanPanelStorageBattery) from a single
snapshot. Gen3 clients leave these fields None; Gen2-only entities are
capability-gated in the integration.

Fields added to SpanPanelSnapshot:
- feedthrough_power_w, feedthrough_energy_produced/consumed_wh
- main_meter_energy_produced/consumed_wh
- current_run_config
- hardware_door_state, hardware_uptime, hardware_proximity_proven
- hardware_is_ethernet/wifi/cellular_connected
- hardware_update_status, hardware_env, hardware_manufacturer, hardware_model

Updated SpanPanelClient.get_snapshot() to populate all new fields from
the existing get_status(), get_panel_state(), get_circuits() calls.
The original _parse_instances() computed circuit_id as
instance_id - METRIC_IID_OFFSET (hardcoded 27), reverse-engineered from
one MAIN40 where trait 26 IIDs happened to be 28-52. On the MLO48,
trait 26 IIDs are [2, 35, 36, ...] — the offset differs, so most
computed circuit_ids were out of range and silently discarded, leaving
the panel with no circuits discovered. Reported in PR #169.

Two bugs fixed:

1. Offset-based circuit_id: replaced with positional pairing. Trait 16
   and trait 26 IIDs are now collected independently, sorted,
   deduplicated, and paired by position (circuit_id = idx + 1). Works
   correctly regardless of actual IID values or panel model.

2. GetRevision instance_id: _get_circuit_name() was passing the
   positional circuit_id as the trait 16 instance ID. On the MAIN40
   this accidentally worked (IIDs 1-25 match positions); on the MLO48
   trait 16 IIDs are non-contiguous so names were fetched from wrong
   instances. CircuitInfo now stores name_iid (the actual trait 16 IID)
   and _fetch_circuit_names() uses it directly.

Also adds _metric_iid_to_circuit reverse map built at connect time for
O(1) streaming lookup, replacing the broken IID-offset arithmetic in
_decode_and_store_metric().

Removes METRIC_IID_OFFSET from grpc/const.py — the constant embodied
the incorrect assumption.

Updates grpc-transport-design.md with root cause analysis and fix.
Covers editable install workflow for both local HA core and Docker
container deployments, debug logging config, diagnostic symptom table,
and iteration workflow for protobuf decoder fixes.
Replace positional pairing with Breaker Group (BG) based mapping.
Each BG IID equals its corresponding trait 26 metric IID and contains
an explicit reference to the trait 16 name IID, eliminating fragile
positional assumptions.

Changes:
- Add _fetch_breaker_groups() using trait 15 as authoritative source
- Add _query_breaker_group() to parse single/dual-phase BG instances
- Add _extract_trait_ref_iid() helper for protobuf ref extraction
- Add breaker_position field to CircuitInfo (physical slot 1-48)
- Detect dual-phase circuits (field 11=single, field 13=dual)
- Build _metric_iid_to_circuit reverse map for O(1) stream lookup
- Filter orphan metric IIDs (e.g. 2, 401, 402) automatically
- Fall back to positional pairing if no BG instances available

Validated on MAIN 40 (25 circuits, 4 dual-phase) and MLO 48
(31 circuits, 10 dual-phase) — all correct.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude and others added 9 commits February 18, 2026 23:47
Gen3 gRPC does not expose serial/firmware via a dedicated trait yet.
The panel_resource_id (captured during instance discovery) serves as a
unique, stable panel identifier that can be used for entity unique_id
generation in Home Assistant.

Without this fix, serial_number is empty and HA sensors cannot be
registered in the entity registry (no unique_id = no persistent entity).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a check before the deps-installed condition to detect when the
virtual environment has been recreated (e.g. for a new Python version)
but the .deps-installed marker still reflects the old install. Uses
pre_commit importability as the sentinel since it is always a dev
dependency.

Also add exit code checking for `poetry run pre-commit install` so
failures are surfaced rather than silently swallowed.
Home Assistant now requires Python >=3.14.2. Update mypy python_version
from 3.13 to 3.14 so type checking reflects the actual runtime. Update
black target-version from py312 to py313 (py314 not yet supported by
black 25.1.0).
Extract _decode_main_feed_leg() from _decode_main_feed() and
_parse_instance_item() from _parse_instances() to bring both methods
below the complexity threshold flagged by CodeFactor.

- _decode_main_feed: CC 20 -> 7 (D -> B)
- _parse_instances: CC 23 -> 13 (D -> C)

Also fix pre-existing type and attribute issues surfaced by mypy/pylint
when the file was first staged:
- _extract_trait_ref_iid: broaden parameter to ProtobufValue | None and
  fix the return type to always produce int
- _parse_breaker_group: remove unnecessary "or b""" coercions now that
  _extract_trait_ref_iid handles None directly
- Initialize _raw_bg_iids/_raw_name_iids/_raw_metric_iids in __init__
  to satisfy pylint attribute-defined-outside-init
Split the monolithic README into a concise top-level overview with
dedicated detail pages:

- README.md: high-level introduction, quick start via create_span_client,
  Gen2 vs Gen3 capability table, documentation table of contents
- docs/gen2-client.md: connection patterns, auth, full API reference,
  timeout/retry/caching, Home Assistant integration, simulation mode
- docs/gen3-client.md: gRPC usage, streaming callbacks, snapshot model,
  low-level PanelData access, error handling
- docs/error-handling.md: exception hierarchy, HTTP to exception mapping,
  retry configuration, Gen3 gRPC errors
- docs/development.md: setup, test/lint commands, project structure,
  OpenAPI client regeneration, Gen3 internals, contributing guide
@cayossarian cayossarian merged commit 706661f into main Feb 20, 2026
7 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.

2 participants

Comments