Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
51ec134
Add gRPC transport layer with PanelCapability flags and unified snaps…
cayossarian Feb 17, 2026
47b910e
Phase 2a: extend SpanPanelSnapshot with Gen2 panel and hardware statu…
cayossarian Feb 17, 2026
e75eb52
fix(grpc): replace fixed IID offset with positional circuit pairing
cayossarian Feb 17, 2026
08b67e9
docs: add Developer Setup for Hardware Testing section
cayossarian Feb 17, 2026
3e854f3
docs: add v1.1.15 changelog entry for Gen3 gRPC support
cayossarian Feb 18, 2026
d8f918f
fix(grpc): use Trait 15 Breaker Groups for authoritative circuit mapping
Feb 18, 2026
0389f6f
Merge remote-tracking branch 'origin/main' into grpc_addition
cayossarian Feb 18, 2026
51d9c25
Merge remote-tracking branch 'origin/main' into grpc_addition
cayossarian Feb 18, 2026
cda7c56
Merge remote-tracking branch 'origin/main' into grpc_addition
cayossarian Feb 18, 2026
2e946dd
chore: regenerate poetry.lock
cayossarian Feb 18, 2026
df2d114
Merge branch 'grpc_addition' of github.com:SpanPanel/span-panel-api i…
cayossarian Feb 18, 2026
5f8277f
fix: use panel_resource_id as serial number fallback for Gen3
Feb 19, 2026
05141e3
fix: detect stale .deps-installed marker when venv is recreated
cayossarian Feb 19, 2026
8929af9
chore: update mypy and black target versions for Python 3.14
cayossarian Feb 19, 2026
7545475
refactor: reduce cyclomatic complexity in grpc/client.py
cayossarian Feb 19, 2026
e3db1b0
chore: regenerate poetry.lock for Python 3.14 compatibility
cayossarian Feb 19, 2026
ccf2a7e
ci: replace Python 3.12 with 3.14 to match HA minimum requirement
cayossarian Feb 19, 2026
e767157
Merge remote-tracking branch 'origin/main' into grpc_addition
cayossarian Feb 19, 2026
cb0ea43
docs: restructure README for Gen2/Gen3 dual-transport support
cayossarian Feb 20, 2026
9b979b5
update change log date
cayossarian Feb 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.15] - 2/19/2026

### Added

- **Gen3 gRPC transport** (`grpc/` subpackage): `SpanGrpcClient` connects to Gen3 panels (MAIN40 / MLO48) on port 50065 via manual protobuf encoding. Supports push-streaming via `Subscribe` RPC with registered callbacks. No authentication required. Thanks
to @Griswoldlabs for the Gen3 implementation (PR #169 in `SpanPanel/span`).
- **Protocol abstraction**: `SpanPanelClientProtocol` and capability-mixin protocols (`AuthCapableProtocol`, `CircuitControlProtocol`, `StreamingCapableProtocol`, etc.) provide static type-safe dispatch across transports.
- **`PanelCapability` flags**: Runtime advertisement of transport features. Gen2 advertises `GEN2_FULL`; Gen3 advertises `GEN3_INITIAL` (`PUSH_STREAMING` only).
- **Unified snapshot model**: `SpanPanelSnapshot` and `SpanCircuitSnapshot` are returned by `get_snapshot()` on both transports. Gen2- and Gen3-only fields are `None` where not applicable.
- **`create_span_client()` factory** (`factory.py`): Creates the appropriate client by generation or auto-detects by probing Gen2 HTTP then Gen3 gRPC.
- **Circuit IID mapping fix**: `_parse_instances()` now collects trait-16 and trait-26 IIDs independently, deduplicates and sorts both lists, and pairs them by position. A `_metric_iid_to_circuit` reverse map enables O(1) streaming lookup. Replaces the
hardcoded `METRIC_IID_OFFSET` assumption that failed on MLO48 panels.
- **gRPC exception classes**: `SpanPanelGrpcError`, `SpanPanelGrpcConnectionError`.
- **`grpcio` optional dependency**: Install with `span-panel-api[grpc]` for Gen3 support.

## [1.1.14] - 12/2025

### Fixed in v1.1.14
Expand Down
561 changes: 48 additions & 513 deletions README.md

Large diffs are not rendered by default.

375 changes: 375 additions & 0 deletions docs/Dev/grpc-transport-design.md

Large diffs are not rendered by default.

155 changes: 155 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Development Guide

## Prerequisites

- Python 3.12 or 3.13
- [Poetry](https://python-poetry.org/) for dependency management

## Setup

```bash
git clone <repository-url>
cd span-panel-api

# Activate the Poetry-managed environment
eval "$(poetry env activate)"

# Install all dependencies including dev extras
poetry install

# Install pre-commit hooks
poetry run pre-commit install
```

## Running Tests

```bash
# Full test suite
poetry run pytest

# With verbose output
poetry run pytest -v

# Specific test file
poetry run pytest tests/test_core_client.py -v

# With coverage
poetry run pytest --cov=span_panel_api tests/

# Generate HTML coverage report
python scripts/coverage.py --full

# Check coverage meets the threshold
python scripts/coverage.py --check --threshold 90
```

## Code Quality

```bash
# Run all pre-commit hooks on all files (lint, format, type-check, security)
poetry run pre-commit run --all-files

# Lint only
poetry run ruff check src/span_panel_api/

# Format code
poetry run ruff format src/span_panel_api/

# Type checking
poetry run mypy src/span_panel_api/

# Security audit
poetry run bandit -c pyproject.toml -r src/span_panel_api/
```

## Project Structure

```text
span-panel-api/
├── src/span_panel_api/ # Main library
│ ├── __init__.py # Public API surface
│ ├── client.py # SpanPanelClient — Gen2 REST client
│ ├── factory.py # create_span_client — auto-detect factory
│ ├── protocol.py # Protocol definitions for type-safe dispatch
│ ├── models.py # Transport-agnostic data models
│ ├── simulation.py # Simulation engine (Gen2 only)
│ ├── exceptions.py # Exception hierarchy
│ ├── const.py # HTTP status constants
│ ├── phase_validation.py # Solar / phase utilities
│ ├── generated_client/ # Auto-generated OpenAPI client (do not edit)
│ └── grpc/ # Gen3 gRPC client
│ ├── client.py # SpanGrpcClient
│ ├── models.py # Low-level gRPC data models
│ └── const.py # gRPC constants (port, trait IDs, etc.)
├── tests/ # Test suite
│ ├── test_core_client.py
│ ├── test_context_manager.py
│ ├── test_cache_functionality.py
│ ├── test_enhanced_circuits.py
│ ├── test_simulation_mode.py
│ ├── test_factories.py
│ ├── conftest.py
│ └── simulation_fixtures/ # Pre-recorded API response fixtures
├── examples/ # Example scripts and simulation configs
├── scripts/ # Developer utility scripts
├── docs/ # This documentation
├── openapi.json # SPAN Panel OpenAPI specification (Gen2)
└── pyproject.toml # Poetry / project configuration
```

## Updating the Gen2 OpenAPI Client

The `generated_client/` directory is auto-generated from `openapi.json`. Do not edit it manually.

1. Obtain a fresh `openapi.json` from a live panel:

```text
GET http://<panel-ip>/api/v1/openapi.json
```

2. Replace `openapi.json` in the repo root.

3. Regenerate:

```bash
poetry run python generate_client.py
```

4. Update `src/span_panel_api/client.py` if the API surface changed.

5. Add or update tests for any changed behaviour.

## Gen3 gRPC Development

The Gen3 client uses manual protobuf encoding/decoding to avoid generated stubs, keeping the dependency surface to the single optional `grpcio` package.

Key files:

- `grpc/client.py` — `SpanGrpcClient` implementation, protobuf helpers, metric decoders
- `grpc/models.py` — `CircuitInfo`, `CircuitMetrics`, `PanelData`
- `grpc/const.py` — port number, trait IDs, product identifiers

The gRPC client connects to `TraitHandlerService` at port 50065 and uses three RPC methods:

| RPC | Purpose |
| -------------- | -------------------------------- |
| `GetInstances` | Discover circuit trait instances |
| `GetRevision` | Fetch circuit names by trait IID |
| `Subscribe` | Stream real-time power metrics |

## Adding a New Feature

1. If adding a new API capability, update `PanelCapability` in `models.py`.
2. If adding a new method to both transports, add it to the appropriate `Protocol` in `protocol.py`.
3. Add type hints and docstrings to all new public functions and classes.
4. Write tests covering the new code (target > 80% coverage for new code).
5. Update the relevant `docs/` page.

## Release Process

Versioning follows [Semantic Versioning](https://semver.org/).

1. Update `__version__` in `src/span_panel_api/__init__.py`.
2. Update `CHANGELOG.md`.
3. Run the full test suite and pre-commit hooks.
4. Tag and push — CI will publish to PyPI automatically.
142 changes: 142 additions & 0 deletions docs/error-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Error Handling and Retry

## Exception Hierarchy

All exceptions inherit from `SpanPanelError`.

```text
SpanPanelError
├── SpanPanelAuthError — authentication failures (401, 403)
├── SpanPanelConnectionError — network errors or unreachable panel
├── SpanPanelTimeoutError — request timeout
├── SpanPanelValidationError — invalid input or schema mismatch
├── SpanPanelAPIError — general API error (catch-all for HTTP errors)
├── SpanPanelRetriableError — transient server errors (502, 503, 504)
├── SpanPanelServerError — non-retriable server error (500)
├── SpanPanelGrpcError — base for Gen3 gRPC errors
│ └── SpanPanelGrpcConnectionError — Gen3 connection failure
└── SimulationConfigurationError — invalid simulation config (simulation mode only)
```

### Import

```python
from span_panel_api import (
SpanPanelError,
SpanPanelAuthError,
SpanPanelConnectionError,
SpanPanelTimeoutError,
SpanPanelValidationError,
SpanPanelAPIError,
SpanPanelRetriableError,
SpanPanelServerError,
SpanPanelGrpcError,
SpanPanelGrpcConnectionError,
SimulationConfigurationError,
)
```

## HTTP Error → Exception Mapping (Gen2)

| HTTP Status | Exception | Retriable | Action |
| ------------------ | ------------------------------ | -------------------- | ------------------------------ |
| 401, 403 | `SpanPanelAuthError` | Once (after re-auth) | Re-authenticate then retry |
| 500 | `SpanPanelServerError` | No | Check server; report issue |
| 502, 503, 504 | `SpanPanelRetriableError` | Yes | Retry with exponential backoff |
| 404, 400, etc. | `SpanPanelAPIError` | Case-by-case | Check request parameters |
| Timeout | `SpanPanelTimeoutError` | Yes | Retry with backoff |
| Validation failure | `SpanPanelValidationError` | No | Fix input data |
| Simulation config | `SimulationConfigurationError` | No | Fix simulation config file |

The underlying HTTP client is configured with `raise_on_unexpected_status=True`, so unexpected status codes are never silently ignored.

## Handling Errors in Practice

```python
from span_panel_api import (
SpanPanelAuthError,
SpanPanelRetriableError,
SpanPanelTimeoutError,
SpanPanelValidationError,
SpanPanelAPIError,
)

async def fetch_circuits(client):
try:
return await client.get_circuits()
except SpanPanelAuthError:
# Token expired or not yet authenticated — re-auth and retry once
await client.authenticate("my-app", "My Application")
return await client.get_circuits()
except SpanPanelRetriableError as exc:
# Temporary server overload — let retry logic or coordinator handle this
logger.warning("Transient server error, will retry: %s", exc)
raise
except SpanPanelTimeoutError as exc:
# Network too slow — retry after backoff
logger.warning("Request timed out: %s", exc)
raise
except SpanPanelValidationError as exc:
# Unexpected response structure — not retriable
logger.error("Validation error: %s", exc)
raise
except SpanPanelAPIError as exc:
# Any other API error
logger.error("API error: %s", exc)
raise
```

## Retry Configuration (Gen2)

Configure retries on the client to handle transient network issues automatically:

```python
from span_panel_api import SpanPanelClient

client = SpanPanelClient(
"192.168.1.100",
timeout=10.0,
retries=3, # 3 retries → up to 4 total attempts
retry_timeout=0.5, # initial delay before first retry
retry_backoff_multiplier=2.0, # delays: 0.5s, 1.0s, 2.0s
)
```

Only `SpanPanelRetriableError` and `SpanPanelTimeoutError` trigger automatic retries. `SpanPanelAuthError` and `SpanPanelValidationError` are not retried automatically.

### Retry Attempt Count

| `retries` | Total attempts |
| ----------- | -------------- |
| 0 (default) | 1 |
| 1 | 2 |
| 2 | 3 |
| 3 | 4 |

Settings can be changed at runtime:

```python
client.retries = 2
client.retry_timeout = 1.0
client.retry_backoff_multiplier = 1.5
```

## Gen3 gRPC Errors

Gen3 errors use a separate, simpler hierarchy since gRPC does not use HTTP status codes:

```python
from span_panel_api import SpanPanelGrpcError, SpanPanelGrpcConnectionError

try:
await client.connect()
snapshot = await client.get_snapshot()
except SpanPanelGrpcConnectionError as exc:
# Panel unreachable or gRPC channel failed
logger.error("Gen3 connection failed: %s", exc)
except SpanPanelGrpcError as exc:
# Other gRPC-level errors
logger.error("Gen3 gRPC error: %s", exc)
```

Gen3 does not have built-in retry logic — reconnect handling should be implemented at the integration layer (e.g., the Home Assistant coordinator).
Loading