Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- name: Install dependencies
run: |
# Replace path dependencies with PyPI versions for CI
sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = ">=2.2.2"/' pyproject.toml
sed -i 's/span-panel-api = {path = "..\/span-panel-api", develop = true}/span-panel-api = ">=2.2.3"/' pyproject.toml
sed -i 's/ha-synthetic-sensors = {path = "..\/ha-synthetic-sensors", develop = true}/ha-synthetic-sensors = "^1.1.13"/' pyproject.toml
# Regenerate lock file with the modified dependencies
poetry lock
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to this project will be documented in this file.

## [2.0.2] - 3/2026

### Changed

- **Panel size always available** — `panel_size` is now sourced from the Homie schema by the underlying `span-panel-api` library during connection,
replacing a heuristic that could undercount when trailing breaker positions were empty. Previously, panels with unused trailing breaker slots
reported a smaller panel size, which understated the unmapped-sensor count and caused the
[span-card](https://github.com/SpanPanel/span-card) layout to show fewer slots than physically present. The `panel_size` sensor attribute and
WebSocket topology field are now unconditionally populated.
- Requires `span-panel-api>=2.2.3`

## [2.0.1] - 3/2026

⚠️ **STOP — If your SPAN panel is not on firmware `spanos2/r202603/05` or later, do not upgrade. Ensure you are on v1.3.0 or later BEFORE upgrading to
Expand Down
4 changes: 2 additions & 2 deletions custom_components/span_panel/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"iot_class": "local_push",
"issue_tracker": "https://github.com/SpanPanel/span/issues",
"requirements": [
"span-panel-api>=2.2.2"
"span-panel-api>=2.2.3"
],
"version": "2.0.1",
"version": "2.0.2",
"zeroconf": [
{
"type": "_span._tcp.local."
Expand Down
3 changes: 1 addition & 2 deletions custom_components/span_panel/sensor_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@ def extra_state_attributes(self) -> dict[str, Any] | None:
snapshot = self.coordinator.data
attributes: dict[str, Any] = {}

if snapshot.panel_size is not None:
attributes["panel_size"] = snapshot.panel_size
attributes["panel_size"] = snapshot.panel_size
if snapshot.wifi_ssid is not None:
attributes["wifi_ssid"] = snapshot.wifi_ssid

Expand Down
50 changes: 50 additions & 0 deletions tests/test_panel_sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,56 @@ def test_dsm_state_with_real_data(self) -> None:
snapshot_off = SpanPanelSnapshotFactory.create(dsm_state=DSM_OFF_GRID)
assert dsm_description.value_fn(snapshot_off) == DSM_OFF_GRID

def test_software_version_extra_state_attributes_panel_size(
self, mock_coordinator: MagicMock
) -> None:
"""Test that panel_size is always present in extra_state_attributes."""
from custom_components.span_panel.sensor import SpanPanelStatus
from custom_components.span_panel.sensor_definitions import STATUS_SENSORS

description = next(d for d in STATUS_SENSORS if d.key == "software_version")

snapshot_with_size = SpanPanelSnapshotFactory.create(panel_size=32)
mock_coordinator.data = snapshot_with_size
sensor = SpanPanelStatus(mock_coordinator, description, snapshot_with_size)
attrs = sensor.extra_state_attributes
assert attrs is not None
assert attrs["panel_size"] == 32

def test_software_version_extra_state_attributes_wifi_ssid(
self, mock_coordinator: MagicMock
) -> None:
"""Test that wifi_ssid appears in extra_state_attributes when present."""
from custom_components.span_panel.sensor import SpanPanelStatus
from custom_components.span_panel.sensor_definitions import STATUS_SENSORS

description = next(d for d in STATUS_SENSORS if d.key == "software_version")

snapshot = SpanPanelSnapshotFactory.create(panel_size=24, wifi_ssid="MyNetwork")
mock_coordinator.data = snapshot
sensor = SpanPanelStatus(mock_coordinator, description, snapshot)
attrs = sensor.extra_state_attributes
assert attrs is not None
assert attrs["panel_size"] == 24
assert attrs["wifi_ssid"] == "MyNetwork"

def test_software_version_extra_state_attributes_no_wifi(
self, mock_coordinator: MagicMock
) -> None:
"""Test that wifi_ssid is omitted when None."""
from custom_components.span_panel.sensor import SpanPanelStatus
from custom_components.span_panel.sensor_definitions import STATUS_SENSORS

description = next(d for d in STATUS_SENSORS if d.key == "software_version")

snapshot = SpanPanelSnapshotFactory.create(panel_size=16, wifi_ssid=None)
mock_coordinator.data = snapshot
sensor = SpanPanelStatus(mock_coordinator, description, snapshot)
attrs = sensor.extra_state_attributes
assert attrs is not None
assert "wifi_ssid" not in attrs
assert attrs["panel_size"] == 16

def test_dsm_grid_state_deprecated_alias(self) -> None:
"""Test DSM Grid State reads from dsm_state (deprecated alias)."""
from custom_components.span_panel.const import DSM_ON_GRID
Expand Down
Loading