Skip to content

Add Gen3 (MAIN 40 / MLO 48) gRPC support#169

Open
Griswoldlabs wants to merge 4 commits intoSpanPanel:mainfrom
Griswoldlabs:gen3-grpc-support
Open

Add Gen3 (MAIN 40 / MLO 48) gRPC support#169
Griswoldlabs wants to merge 4 commits intoSpanPanel:mainfrom
Griswoldlabs:gen3-grpc-support

Conversation

@Griswoldlabs
Copy link

Gen3 (MAIN 40 / MLO 48) gRPC Support

Adds local gRPC-based support for Gen3 Span panels alongside existing Gen2 REST support. Gen2 code is completely untouched — Gen3 activates only via auto-detection in the config flow.

What this does

  • Auto-detects Gen2 vs Gen3 during config flow (REST → gRPC fallback)
  • Gen3 panels communicate via gRPC on port 50065 (no auth required)
  • Real-time push-based streaming power/voltage/current/frequency data
  • Breaker state monitoring via voltage threshold detection
  • Gen2 code is completely untouched — zero risk to existing users

Architecture

  • New gen3/ subdirectory with isolated Gen3 code path
  • SpanGrpcClient: raw protobuf parsing on port 50065 (no generated stubs needed)
  • SpanGen3Coordinator: wraps push-based streaming in HA's DataUpdateCoordinator pattern
  • Pluggable design — Gen3 gRPC client can be extracted to a span-panel-grpc package later

Config Flow Detection Logic

User enters host IP
  → Try REST: GET /api/v1/status (existing validate_host)
    → Success → Gen2 panel, continue existing auth flow
    → Failure →
      → Try gRPC: connect to port 50065, send GetInstances (5s timeout)
        → Success → Gen3 panel, skip auth, create entry with panel_gen="gen3"
        → Failure → "Cannot connect to panel"

What's included

  • Per-circuit: power, voltage, current sensors + breaker binary sensor
  • Main feed: power, voltage, current, frequency sensors
  • Config flow auto-detection (REST → gRPC fallback)
  • Proto descriptor set file (shipped for reference/future use)

What's NOT included (future PRs)

  • Circuit relay control via gRPC UpdateState RPC
  • Energy accumulation (Gen3 gRPC doesn't provide this yet)
  • Solar sensor combining
  • MLO 48 testing (may work — same gRPC protocol suspected)

Modified files (minimal, surgical changes)

File Lines added Change
const.py +3 Add CONF_PANEL_GEN constant
manifest.json +2 Add grpcio>=1.60.0 dependency, bump to v1.4.0
config_flow.py +30 gRPC fallback in user step + _test_gen3_connection() helper
__init__.py +43 Gen3 setup/unload routing (early return branches)
sensor.py +10 Gen3 sensor factory early return
binary_sensor.py +9 Gen3 binary sensor factory early return

New files (all under gen3/)

File Lines Purpose
__init__.py 1 Package marker
const.py 30 Gen3 constants (trait IDs, gRPC port, voltage threshold)
span_grpc_client.py ~750 gRPC client with raw protobuf parsing
coordinator.py ~90 DataUpdateCoordinator wrapper for push-based streaming
sensors.py ~260 Main feed + per-circuit sensor entities
binary_sensors.py ~90 Breaker ON/OFF binary sensors
span.protoset binary Proto descriptor set (documentation/future use)

Testing

  • Tested on MAIN 40 with 25 circuits (104 entities created)
  • Gen2 path is completely unchanged — zero risk to existing users
  • All Gen3 entities appear with correct values matching the Span app
  • gRPC streaming updates entities in real-time

Related

Background

We (GriswoldLabs) reverse-engineered the Gen3 gRPC protocol and built a standalone integration that's been running successfully. Per Discussion #168, @cayossarian offered org admin access for a PR with a pluggable architecture. This PR implements that as an additive, non-breaking change with Gen3 code fully isolated in its own subdirectory.

🤖 Generated with Claude Code

Adds local gRPC-based support for Gen3 Span panels alongside existing
Gen2 REST support. Gen2 code is completely untouched — Gen3 activates
only via auto-detection in the config flow.

Architecture:
- New gen3/ subdirectory with isolated Gen3 code path
- SpanGrpcClient: connects to port 50065, raw protobuf parsing
- SpanGen3Coordinator: wraps push-based streaming in DataUpdateCoordinator
- Config flow auto-detects Gen2 vs Gen3 (REST → gRPC fallback)
- Gen3 panels require no authentication

Entities:
- Main feed: power, voltage, current, frequency sensors
- Per-circuit: power, voltage, current sensors + breaker binary sensor
- Device hierarchy: panel → circuit sub-devices

Not included (future PRs):
- Circuit relay control via gRPC UpdateState RPC
- Energy accumulation (Gen3 gRPC doesn't provide this yet)
- Solar sensor combining

Closes SpanPanel#96
Relates to SpanPanel#98

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The METRIC_IID_OFFSET was hardcoded to 27, which only worked for panels
where trait 26 (metrics) IIDs start at 28. On panels with different
numbering, this caused names to pair with wrong power readings.

Now dynamically discovers both trait 16 (name) and trait 26 (metric)
instance IDs during setup and pairs them by sorted position, making the
mapping work regardless of the panel's IID numbering scheme.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Griswoldlabs
Copy link
Author

Thanks for testing this @cecilkootz! You're right — there was a bug in the circuit mapping.

Root cause: The mapping between circuit names (trait 16) and power metrics (trait 26) used a hardcoded offset of 27 between the two instance ID spaces. This was reverse-engineered from one specific MAIN 40 panel where trait 16 IIDs are 1-25 and trait 26 IIDs are 28-52. On your panel the numbering is likely different, so names were getting paired with the wrong power readings.

Fix (just pushed): Instead of assuming a fixed offset, the integration now discovers both trait 16 and trait 26 instance IDs during setup, sorts them, and pairs them by position. This should work correctly regardless of how your panel numbers its instances.

Could you try the updated branch and let me know if the mapping is correct now? If you're still seeing mismatches, enabling debug logging for custom_components.span_panel.gen3 would help — the discovery phase now logs the IID mapping for each circuit.

@cecilkootz
Copy link

cecilkootz commented Feb 17, 2026

Thanks for testing this @cecilkootz! You're right — there was a bug in the circuit mapping.

Root cause: The mapping between circuit names (trait 16) and power metrics (trait 26) used a hardcoded offset of 27 between the two instance ID spaces. This was reverse-engineered from one specific MAIN 40 panel where trait 16 IIDs are 1-25 and trait 26 IIDs are 28-52. On your panel the numbering is likely different, so names were getting paired with the wrong power readings.

Fix (just pushed): Instead of assuming a fixed offset, the integration now discovers both trait 16 and trait 26 instance IDs during setup, sorts them, and pairs them by position. This should work correctly regardless of how your panel numbers its instances.

Could you try the updated branch and let me know if the mapping is correct now? If you're still seeing mismatches, enabling debug logging for custom_components.span_panel.gen3 would help — the discovery phase now logs the IID mapping for each circuit.

Yeah. Let me get the update pushed and try again. Sorry for the original comment removal. I have a MLO48 and wanted to debug further before raising an issue.

2026-02-17 15:56:37.584 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Discovered 31 name instances (trait 16) and 36 metric instances (trait 26, excl main feed). Name IIDs: [1, 2, 3, 4, 5], Metric IIDs: [2, 35, 35, 36, 37]
2026-02-17 15:56:37.584 WARNING (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Trait 16 has 31 instances but trait 26 has 36 — pairing by position (some circuits may be unnamed)
2026-02-17 15:56:37.595 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 1 (name_iid=1, metric_iid=2): Bedroom #2 / Hall Lighting
2026-02-17 15:56:37.605 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 2 (name_iid=2, metric_iid=35): Bedroom #4 / Smoke Detectors
2026-02-17 15:56:37.617 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 3 (name_iid=3, metric_iid=35): Garage Door Opener / Garage Lighting
2026-02-17 15:56:37.627 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 4 (name_iid=4, metric_iid=36): Master Bedroom / Laundry Lights
2026-02-17 15:56:37.639 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 5 (name_iid=5, metric_iid=37): Microwave
2026-02-17 15:56:37.650 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 6 (name_iid=6, metric_iid=38): Dishwasher / Disposal
2026-02-17 15:56:37.661 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 7 (name_iid=7, metric_iid=39): Kitchen G.F.I. / Refrigerator
2026-02-17 15:56:37.671 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 8 (name_iid=8, metric_iid=40): Wine Cooler
2026-02-17 15:56:37.719 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 9 (name_iid=9, metric_iid=41): Kitchen Island G.F.I. Recepticles
2026-02-17 15:56:37.730 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 10 (name_iid=10, metric_iid=42): Laundry Room / Washer
2026-02-17 15:56:37.740 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 11 (name_iid=11, metric_iid=43): Bathroom G.F.I. Recepticles
2026-02-17 15:56:37.751 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 12 (name_iid=12, metric_iid=44): Kitchen G.F.I. Recepticles
2026-02-17 15:56:37.764 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 13 (name_iid=13, metric_iid=45): Master Bathroom / Hall Bathroom
2026-02-17 15:56:37.775 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 14 (name_iid=14, metric_iid=46): Garage G.F.I. Recepticles
2026-02-17 15:56:37.787 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 15 (name_iid=15, metric_iid=47): Foyer / Front Entry / Dining Room Lights
2026-02-17 15:56:37.797 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 16 (name_iid=16, metric_iid=48): Pocket Office / Kitchen Lighting
2026-02-17 15:56:37.807 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 17 (name_iid=17, metric_iid=49): Family Room / Back Porch
2026-02-17 15:56:37.818 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 18 (name_iid=18, metric_iid=50): Bedroom #3 / Servers
2026-02-17 15:56:37.830 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 19 (name_iid=19, metric_iid=51): Bonus Room / Stairway Lights
2026-02-17 15:56:37.841 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 20 (name_iid=21, metric_iid=52): Downstairs Central Air
2026-02-17 15:56:37.852 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 21 (name_iid=23, metric_iid=53): Cooktop
2026-02-17 15:56:37.863 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 22 (name_iid=24, metric_iid=55): Water Heater
2026-02-17 15:56:37.874 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 23 (name_iid=25, metric_iid=57): Downstairs Air Conditioning
2026-02-17 15:56:37.886 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 24 (name_iid=26, metric_iid=58): Laundry Dryer
2026-02-17 15:56:37.896 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 25 (name_iid=27, metric_iid=59): Upstairs Air Conditioning
2026-02-17 15:56:37.906 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 26 (name_iid=28, metric_iid=60): Upstairs Central Air
2026-02-17 15:56:37.916 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 27 (name_iid=29, metric_iid=61): Big Garage Air Conditioning
2026-02-17 15:56:37.927 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 28 (name_iid=30, metric_iid=62): EV Charger
2026-02-17 15:56:37.938 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 29 (name_iid=31, metric_iid=63): Venthood
2026-02-17 15:56:37.947 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 30 (name_iid=32, metric_iid=64): Dining Room Recepticles
2026-02-17 15:56:37.957 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 31 (name_iid=34, metric_iid=65): Double Oven
2026-02-17 15:56:37.967 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 32 (name_iid=32, metric_iid=68): Dining Room Recepticles
2026-02-17 15:56:37.987 DEBUG (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Circuit 34 (name_iid=34, metric_iid=74): Double Oven
2026-02-17 15:56:38.010 INFO (MainThread) [custom_components.span_panel.gen3.span_grpc_client] Connected to Gen3 panel at 192.168.1.21:50065 — 36 circuits discovered

@haggerty23
Copy link

This PR worked for me except I have two Span panels. But after I used AI coding tools, I was able to create a workaround. When you get a chance, it'd be great to add support for more than one gen3 panel.

@cayossarian
Copy link
Member

@Griswoldlabs I have taken the liberty of refactoring to accommodate your grpc support both in the span-panel-api and the span repo. I can only confirm the gen2 is not broken but hopefully your PR is faithfully represented as well. See the handoff and let me know how I can facilitate any further integration. There are planning docs in docs/dev for each repo that inform as to what changed and why.

Once you are able to test and satisfied with your panel, we can publish a beta and invite others to use it. An org invite will be forthcoming. The credit is all yours.

I have yet to update the readme's but the change logs are updated. Simulation mode only works for gen2 at present, but no big deal.

Thanks on behalf of the community.

Support for reading and exposing the physical breaker slot position from Gen3 panels. It introduces a new "Panel Position" sensor that displays the breaker position (1-48) for each circuit, and refactors the circuit discovery logic to properly resolve breaker group information.
@Griswoldlabs
Copy link
Author

This is incredible — thank you for taking this on so quickly. The architecture looks exactly right: gRPC transport in the library, capabilities-based entity loading, push vs poll auto-selection.

I'll pull both branches and test against my MAIN 40 this week. The main thing I want to verify is that the refactored decoders produce the same readings I validated against the Span app.

The big open item is the name/metric IID count mismatch on @cecilkootz's MLO 48 (31 names vs 36 metrics). My best theory is that 240V dual-phase circuits report two metric IIDs (one per breaker position) but share a single trait 16 name. Trait 15 (Breaker Groups) likely holds the mapping between physical breaker positions and named circuits — I'll look into using that to properly correlate them. Unfortunately I can only test single-phase dedup logic against my MAIN 40, so @cecilkootz's help will be essential for validating the MLO 48 fix.

Looking forward to the org invite. Happy to own the gRPC side going forward.

@Griswoldlabs
Copy link
Author

Glad it's working! Multi-panel support should be doable — the config flow already creates a separate config entry per host, so in theory each panel gets its own coordinator and entity set. The issue is likely unique_id collisions or the config flow not allowing a second Gen3 entry. Can you share what specifically broke with two panels? (e.g., did the second panel fail to add, or did entities from both panels merge together?)

@cecilkootz
Copy link

cecilkootz commented Feb 18, 2026

This is incredible — thank you for taking this on so quickly. The architecture looks exactly right: gRPC transport in the library, capabilities-based entity loading, push vs poll auto-selection.

I'll pull both branches and test against my MAIN 40 this week. The main thing I want to verify is that the refactored decoders produce the same readings I validated against the Span app.

The big open item is the name/metric IID count mismatch on @cecilkootz's MLO 48 (31 names vs 36 metrics). My best theory is that 240V dual-phase circuits report two metric IIDs (one per breaker position) but share a single trait 16 name. Trait 15 (Breaker Groups) likely holds the mapping between physical breaker positions and named circuits — I'll look into using that to properly correlate them. Unfortunately I can only test single-phase dedup logic against my MAIN 40, so @cecilkootz's help will be essential for validating the MLO 48 fix.

Looking forward to the org invite. Happy to own the gRPC side going forward.

I pushed up a PR to your branch with a working version. I did some inspection and think the discovery works (at least on MLO48). Jumped a bit with the other changes going on so may need more work. Thank you for working through this and getting a working solution for Gen3.

==============================================================================================================
  SPAN PANEL - MAIN FEED
==============================================================================================================
  Panel ID:    d457b54377c59cb9
  Power:           3380.2 W
  Voltage:          240.6 V  (Leg A: 120.3V  Leg B: 120.3V)
  Current:          14.05 A
  Frequency:        60.04 Hz

  Breakers:    31 configured, 31 ON, 0 OFF
  Slots:       31 occupied / 48 total

==============================================================================================================
  ACTIVE BREAKERS
==============================================================================================================
  Slot  Name                           State  Phase   Power (W)  Voltage (V)  Current (A)     PF
--------------------------------------------------------------------------------------------------------------
     1  Bedroom #2 / Hall Lighting        ON   120V       150.3        120.7         1.74      -
     3  Bedroom #4 / Smoke Detectors      ON   120V       239.8        108.2         2.62      -
     4  Microwave                         ON   120V         0.9        120.2         0.03      -
     5  Garage Door Opener / Garage Lighting    ON   120V        11.4        120.7         0.18      -
     6  Dishwasher / Disposal             ON   120V         1.0        120.6         0.03      -
     7  Master Bedroom / Laundry Lights    ON   120V        65.9        120.2         0.74      -
     8  Kitchen G.F.I. / Refrigerator     ON   120V      1443.9        120.2        12.07      -
     9  Master Bathroom / Hall Bathroom    ON   120V         0.9        120.6         0.03      -
    10  Wine Cooler                       ON   120V        92.0        120.7         1.06      -
    11  Garage G.F.I. Recepticles         ON   120V        44.2        120.2         0.66      -
    12  Kitchen Island G.F.I. Recepticles    ON   120V         0.2        120.2         0.03      -
    13  Foyer / Front Entry / Dining Room Lights    ON   120V         6.1        120.6         0.10      -
    14  Laundry Room / Washer             ON   120V       202.2        120.6         1.71      -
    15  Pocket Office / Kitchen Lighting    ON   120V        82.5        120.1         0.86      -
    16  Bathroom G.F.I. Recepticles       ON   120V         5.2        120.1         0.16      -
    17  Family Room / Back Porch          ON   120V        76.4        120.7         1.00      -
    18  Kitchen G.F.I. Recepticles        ON   120V        13.1        120.7         0.20      -
    19  Bedroom #3 / Servers              ON   120V       251.4        120.2         2.49      -
    20  Venthood                          ON   120V         0.8        120.3         0.03      -
    21  Bonus Room / Stairway Lights      ON   120V        49.2        120.7         0.92      -
    22  Dining Room Recepticles           ON   120V         3.3        108.5         0.13      -
    23  Double Oven                       ON   240V         1.7        240.9         0.06   0.88
          Leg A:  120.2V    0.04A    |  Leg B:  120.7V    0.03A
    26  Downstairs Air Conditioning       ON   240V         0.0        240.9         0.05   0.01
          Leg A:  120.7V    0.03A    |  Leg B:  120.2V    0.03A
    27  Downstairs Central Air            ON   240V        13.5        240.9         0.19   0.61
          Leg A:  120.2V    0.10A    |  Leg B:  120.6V    0.10A
    30  Laundry Dryer                     ON   240V         0.7        240.8         0.05   0.95
          Leg A:  120.6V    0.03A    |  Leg B:  120.2V    0.02A
    31  Cooktop                           ON   240V         0.1        240.9         0.05   0.07
          Leg A:  120.3V    0.03A    |  Leg B:  120.6V    0.03A
    34  Upstairs Air Conditioning         ON   240V         0.0        241.0         0.05   0.09
          Leg A:  120.7V    0.03A    |  Leg B:  120.3V    0.03A
    35  Water Heater                      ON   240V         0.0        240.8         0.05   0.08
          Leg A:  120.2V    0.03A    |  Leg B:  120.6V    0.03A
    38  Upstairs Central Air              ON   240V        13.3        240.8         0.14   0.90
          Leg A:  120.7V    0.07A    |  Leg B:  120.1V    0.07A
    39  EV Charger                        ON   240V         0.0        240.9         0.05   0.23
          Leg A:  120.2V    0.02A    |  Leg B:  108.6V    0.03A
    42  Big Garage Air Conditioning       ON   240V       464.2        240.8         4.02   0.99
          Leg A:  120.7V    2.01A    |  Leg B:  120.2V    2.01A
--------------------------------------------------------------------------------------------------------------
        TOTAL                                            3234.3

==============================================================================================================
  POWER QUALITY DETAIL
==============================================================================================================
  Slot  Name                              Real (W)  Apparent (VA)  Reactive (VAR)     PF
--------------------------------------------------------------------------------------------------------------
     1  Bedroom #2 / Hall Lighting           150.3           87.4            89.2      -
     3  Bedroom #4 / Smoke Detectors         239.8          151.5           185.5      -
     4  Microwave                              0.9            1.1             2.0      -
     5  Garage Door Opener / Garage Lighting        11.4            8.2            11.7      -
     6  Dishwasher / Disposal                  1.0            0.6             0.7      -
     7  Master Bedroom / Laundry Lights        65.9           36.9            33.3      -
     8  Kitchen G.F.I. / Refrigerator       1443.9          721.9             2.6      -
     9  Master Bathroom / Hall Bathroom         0.9            0.7             1.0      -
    10  Wine Cooler                           92.0           62.7            85.3      -
    11  Garage G.F.I. Recepticles             44.2           33.6            50.6      -
    12  Kitchen Island G.F.I. Recepticles         0.2            0.3             0.5      -
    13  Foyer / Front Entry / Dining Room Lights         6.1            4.4             6.3      -
    14  Laundry Room / Washer                202.2          102.5            34.1      -
    15  Pocket Office / Kitchen Lighting        82.5           48.2            49.9      -
    16  Bathroom G.F.I. Recepticles            5.2            8.8            16.8      -
    17  Family Room / Back Porch              76.4           51.0            67.6      -
    18  Kitchen G.F.I. Recepticles            13.1            8.9            12.1      -
    19  Bedroom #3 / Servers                 251.4          141.9           132.0      -
    20  Venthood                               0.8            1.1             2.0      -
    21  Bonus Room / Stairway Lights          49.2           45.7            77.1      -
    22  Dining Room Recepticles                3.3            3.2             5.3      -
    23  Double Oven                            1.7            1.0             0.9   0.88
    26  Downstairs Air Conditioning            0.0            0.1             0.1   0.01
    27  Downstairs Central Air                13.5           11.0            17.4   0.61
    30  Laundry Dryer                          0.7            0.4             0.0   0.95
    31  Cooktop                                0.1            1.0             2.1   0.07
    34  Upstairs Air Conditioning              0.0            0.2             0.1   0.09
    35  Water Heater                           0.0            0.1             0.1   0.08
    38  Upstairs Central Air                  13.3            7.4             6.4   0.90
    39  EV Charger                             0.0            0.2             0.0   0.23
    42  Big Garage Air Conditioning          464.2          234.9            71.8   0.99

==============================================================================================================
  BREAKER SLOT MAP (48 slots)
==============================================================================================================
            Left (Odd)                       Right (Even)
  ------------------------------    ------------------------------
   1 [ON ] Bedroom #2 / Hall      2  ---
   3 [ON ] Bedroom #4 / Smoke     4 [ON ] Microwave
   5 [ON ] Garage Door Opener     6 [ON ] Dishwasher / Dispo
   7 [ON ] Master Bedroom / L     8 [ON ] Kitchen G.F.I. / R
   9 [ON ] Master Bathroom /     10 [ON ] Wine Cooler
  11 [ON ] Garage G.F.I. Rece    12 [ON ] Kitchen Island G.F
  13 [ON ] Foyer / Front Entr    14 [ON ] Laundry Room / Was
  15 [ON ] Pocket Office / Ki    16 [ON ] Bathroom G.F.I. Re
  17 [ON ] Family Room / Back    18 [ON ] Kitchen G.F.I. Rec
  19 [ON ] Bedroom #3 / Serve    20 [ON ] Venthood
  21 [ON ] Bonus Room / Stair    22 [ON ] Dining Room Recept
  23 [ON ] Double Oven           24  ---
  25  ---                        26 [ON ] Downstairs Air Con
  27 [ON ] Downstairs Central    28  ---
  29  ---                        30 [ON ] Laundry Dryer
  31 [ON ] Cooktop               32  ---
  33  ---                        34 [ON ] Upstairs Air Condi
  35 [ON ] Water Heater          36  ---
  37  ---                        38 [ON ] Upstairs Central A
  39 [ON ] EV Charger            40  ---
  41  ---                        42 [ON ] Big Garage Air Con
  43  ---                        44  ---
  45  ---                        46  ---
  47  ---                        48  ---
==============================================================================================================

The _parse_breaker_group method already distinguishes field 11 (single-pole)
from field 13 (dual-pole) but wasn't propagating that info. Now returns
is_dual_phase and sets it on CircuitInfo.

Tested on MAIN 40: correctly identifies Furnace, Electric dryer, Water heater,
and Electric range as 240V dual-phase circuits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Griswoldlabs
Copy link
Author

Breaker Group Mapping — Validated on Both MAIN 40 and MLO 48 ✓

Great work @cecilkootz! Merged your PR and pushed one small follow-up commit to also propagate is_dual_phase from the BG field type detection.

What Changed

The original positional pairing approach (sorting trait 16 name IIDs and trait 26 metric IIDs, pairing by index) was fundamentally wrong:

  • MAIN 40: 25 names vs 28 metrics — phantom IID 2 shifted ALL 25 names by 1 position
  • MLO 48: 31 names vs 36 metrics — even worse mismatch

Fix: Use Trait 15 (Breaker Groups) as the authoritative mapping source. Each BG instance:

  • Has the same IID as its corresponding metric IID
  • Contains an explicit reference to its trait 16 name IID (no guessing)
  • Identifies single-pole (field 11, 120V) vs dual-pole (field 13, 240V)
  • Contains a BreakerConfig reference with the physical slot number (1-48)

This eliminates the hardcoded offset AND the positional pairing — both were fragile.

Results

MAIN 40 (my panel): 25 circuits, 4 dual-phase, all names and positions correct.
MLO 48 (@cecilkootz): 31 circuits, 10 dual-phase, all names and positions correct.

cecilkootz also added a Panel Position sensor showing the physical breaker slot number for each circuit, which is great for verification and labeling.

For @cayossarian

I've synced the same BG-based mapping fix into the span-panel-api library branch (your grpc_addition). The library version also extracts breaker_position and is_dual_phase. The code structure closely mirrors what cecilkootz did here but adapted to the library's _query_breaker_group() / _parse_breaker_group() pattern.

Happy to push that to span-panel-api once the org invite comes through, or I can PR it against your branch.

@cayossarian
Copy link
Member

@Griswoldlabs the span-panel-api branch is for all intents yours so you can submit the PR from it. I'll do another test and once we are comfortable that the span-panel-api is working as desired we can publish a span version (which also pushes to pypi). At that point we can publish a beta off the span branch, no need to merge that first since there might be feedback that we want to get into the branch before merging to main which would update docs prematurely. Anybody at that point can install the beta from HACs directly.

The org invite is nearly secondary as membership simply allows you to approve other folks PR's and merge. The repo rules are set up so a contributor approval is required prior to merge. This rule is necessary now more than ever to test multiple panel versions.

@Griswoldlabs
Copy link
Author

Library Update: BG mapping fix pushed to span-panel-api

Just pushed the Breaker Group mapping fix to the grpc_addition branch on span-panel-api — commit d8f918f.

What changed (library side)

  • client.py (191 insertions, 27 deletions):

    • _fetch_breaker_groups() — uses trait 15 as the authoritative source for metric→name IID mapping (each BG IID == its metric IID, contains explicit name_iid reference)
    • _query_breaker_group() — parses single-phase (field 11) and dual-phase (field 13) BG instances
    • _extract_trait_ref_iid() — helper for cleaner protobuf trait reference extraction
    • _metric_iid_to_circuit reverse map for O(1) lookup during streaming
    • Orphan metric IIDs (e.g. 2, 401, 402 on MAIN40) filtered automatically
    • Falls back to positional pairing if no BG instances found
  • models.py (+1 line):

    • Added breaker_position: int = 0 to CircuitInfo (physical slot 1–48)

Integration side

The same fix was already merged into the integration fork via @cecilkootz's PR + dual-phase follow-up commit (3e0fbf2). Both integration and library are now in sync.

Deployed & validated

Deployed to a live MAIN 40 panel running Home Assistant — 25 circuits discovered, 4 dual-phase correctly identified, all names and power readings match the Span app. Panel position sensors showing physical slot numbers.

Also validated by @cecilkootz on MLO 48 (31 circuits, 10 dual-phase) — all correct.

@cecilkootz
Copy link

cecilkootz commented Feb 18, 2026

During some more testing tonight I got an alert that HA disk/cpu had an abnormal increase over a sustained period of time. Thinking it perhaps was the unary stream from the channel updating 1/s, I re-used the scan_interval from Gen2 to "throttle" updates. Now pushes to HA are configured with the scan_interval to avoid excessive DB writes. I don't believe data loss will occur as the gRPC client always holds the latest readings. Data loss may occur on disconnect or restart but it's limited to whatever was received in the last interval period. In my case i left the default of 15s.

After doing this I noticed CPU dropped back to normal as did disk IO. This may be a paranoid over reaction. If this was useful, any thoughts on if it should be a new config workflow for Gen3?

As a side, the spikes I was randomly seeing have all but disappeared. Going to let this run over night and see if anything strange shows up.

The spikes cleared up for me with the interval change. 🤷‍♂️
Screenshot 2026-02-18 at 10 15 54

@Griswoldlabs
Copy link
Author

@cecilkootz Good catch on the CPU/disk spike from the 1/s push stream. That's definitely something we need to address before beta.

The gRPC Subscribe stream pushes every second by design (it's how the panel broadcasts), but HA's recorder doesn't need that granularity — 15s is plenty for energy monitoring. Your scan_interval throttle is the right approach.

I think this should be a config flow option for Gen3 with a sensible default (15s to match Gen2). The coordinator can buffer the latest readings from the stream and only push to HA on the interval tick. That way the gRPC client always has fresh data internally (for instant response to manual polls) but HA's recorder isn't overwhelmed.

I'll work this into the library-side coordinator when I test @cayossarian's refactored branches this week.

@haggerty23 Re: multi-panel — the config flow already creates a separate config entry per host, so each panel should get its own coordinator and entity set. The issue is likely unique_id collisions or the config flow blocking a second Gen3 entry. Can you share what specifically broke? (Did the second panel fail during setup, or did entities from both panels merge/conflict?)

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.

MLO 48 & MAIN 40 support

4 participants

Comments