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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ This release requires Home Assistant 2026.8 to avoid a deprecated API — the ol
added each circuit's whole lifetime counter to its offset.
- **A panel this integration can no longer reach makes its entities unavailable instead of reporting 0 W** — a certificate authority that changes mid-session
used to leave every power sensor reading zero and every energy sensor frozen, indefinitely and indistinguishably from a panel drawing no power.
- **A panel that has moved to another address now says so in Repairs**, naming the addresses its certificate does carry so you can point the entry at one with
Reconfigure, instead of retrying silently with every entity unavailable and one line a minute in the log.
- **An add-on restart no longer overwrites the address you configured** — a panel announced by an add-on keeps the host you gave it for as long as that host
still answers, and only the add-on's own ports are taken as published.
- **Recreate entity IDs proposes the ids your panel would produce now** (#252), in your installation's naming style, leaving unique ids and statistics
untouched.
- **The README described Battery Power's sign backwards**, since the sensor has always reported discharging as positive (#184) and only the documentation was
Expand Down Expand Up @@ -132,7 +136,9 @@ This release requires Home Assistant 2026.8 to avoid a deprecated API — the ol
name that is not a fully qualified domain, so the certificate never comes to name it. Set that panel up by its IP address, or by the `.local` name shown in
the mobile app.
- **A discovered address this entry's authority rejects is refused with a log warning and nothing else** — if the panel really has moved, use **Reconfigure** on
the entry, which checks the new address against the standing pin before storing it.
the entry, which checks the new address against the standing pin before storing it. A panel whose certificate does not yet name where it now answers is moved
by reconfiguring to a fully qualified domain name, which asks the panel to regenerate its certificate around it, or to the `.local` name its certificate
already covers; a bare new IP address the certificate does not name is refused, because every connection after the flow would reject it too.

## [2.0.8] - 5/2026

Expand Down
245 changes: 101 additions & 144 deletions README.md

Large diffs are not rendered by default.

29 changes: 27 additions & 2 deletions custom_components/span_panel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
)
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.typing import ConfigType
from span_panel_api import SpanMqttClient, SpanPanelSnapshot, ca_fingerprint
from span_panel_api import (
LeafNameMismatch,
SpanMqttClient,
SpanPanelSnapshot,
ca_fingerprint,
)
from span_panel_api.exceptions import (
SpanPanelAPIError,
SpanPanelAuthError,
Expand Down Expand Up @@ -67,6 +72,7 @@
async_save_panel_settings as async_save_panel_settings,
)
from .graph_horizon import GraphHorizonManager
from .leaf_repairs import async_clear_leaf_name_mismatch, async_raise_leaf_name_mismatch
from .migrations import CURRENT_CONFIG_VERSION, async_migrate_entry # noqa: F401
from .notices import async_forget, async_restore
from .options import SNAPSHOT_UPDATE_INTERVAL
Expand Down Expand Up @@ -350,6 +356,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: SpanPanelConfigEntry) ->
# the one every other integration already shares.
httpx_client=get_async_client(hass),
)

# Before `connect()`, unlike every other subscription here, because
# the library runs the diagnosis this reports *inside* the first
# connect. Registered afterwards it would see nothing on the path it
# exists for: a mismatch makes `connect()` raise
# `SpanPanelConnectionError`, setup raises `ConfigEntryNotReady`, and
# the retry builds a new client -- so every attempt would fire the
# signal into a client with no subscriber and the user would keep
# getting the log line and nothing else.
@callback
def _on_leaf_name_mismatch(mismatch: LeafNameMismatch) -> None:
async_raise_leaf_name_mismatch(hass, entry, mismatch)

entry.async_on_unload(client.register_leaf_mismatch_callback(_on_leaf_name_mismatch))

try:
await client.connect()
except SpanPanelCAChangedError as err:
Expand Down Expand Up @@ -384,8 +405,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: SpanPanelConfigEntry) ->
raise ConfigEntryNotReady(f"SPAN panel is not ready yet: {err}") from err

# The connection got as far as a handshake under the current pin, so
# any standing Repair describes a state that no longer holds.
# any standing Repair describes a state that no longer holds. That
# covers the name mismatch too: the handshake that just succeeded is
# the one whose failure the mismatch explains, and it is the same
# event the library re-arms its own signal on.
async_clear_ca_changed(hass, entry)
async_clear_leaf_name_mismatch(hass, entry)

# The other half of the same condition: the reconnect loop runs
# fire-and-forget, so a CA that changes mid-session cannot surface as
Expand Down
9 changes: 9 additions & 0 deletions custom_components/span_panel/ca_repairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from homeassistant.helpers import issue_registry as ir

from .const import DOMAIN
from .leaf_repairs import async_clear_leaf_name_mismatch

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -52,7 +53,15 @@ def async_raise_ca_changed(
`is_persistent` because the transport this describes is already dead. A
non-persistent issue reloads as a tombstone, and this one has no live state
to re-assert it from — the client that would have noticed is gone.

Supersedes a standing name mismatch on the same entry, and does it here
rather than at each call site so no future one can forget. The two findings
come out of the same failed handshake and its diagnosis, so both can stand
against one panel; only this one carries a decision, and telling somebody to
re-point their configuration at an address served by a certificate this
integration has just refused to trust is the wrong instruction.
"""
async_clear_leaf_name_mismatch(hass, entry)
_LOGGER.error(
"SPAN panel %s is advertising CA %s where %s was pinned. Not reconnecting: a "
"rotated CA and an intercepted connection look identical from here. Confirm the "
Expand Down
Loading
Loading