Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
97a255b
Guard against review IDs in public files
Varitras Nov 1, 2026
0fdb7ea
Drop review IDs from comments and docstrings
Varitras Nov 1, 2026
b06652c
Make comments and docstrings say what the code does
Varitras Nov 1, 2026
893a016
Test a single definition load and a copy-free read
Varitras Nov 1, 2026
779a0da
Load the definition once and drop unused client surface
Varitras Nov 1, 2026
97f18fc
Bound test and job time, check the tag first
Varitras Nov 1, 2026
9acfb71
Read every file for review IDs, spare MAC octets
Varitras Nov 1, 2026
9ae5eb4
Remove the last review IDs and correct stale notes
Varitras Nov 1, 2026
7d4b9c0
Test the serial permission on a serial-keyed entry
Varitras Nov 1, 2026
b60b4cb
End the serial permission when the serial is confirmed
Varitras Nov 1, 2026
af58045
Test the flows against another charger and password
Varitras Nov 1, 2026
1ccc73f
Check the known serial before the flows send a password
Varitras Nov 1, 2026
8c7eb35
Test the client's log levels and reconnect backoff
Varitras Nov 1, 2026
cb4f0e0
Log refusals, connects and stalled retries at debug
Varitras Nov 1, 2026
958c3c9
Test a firmware install of a version not offered
Varitras Nov 1, 2026
d3cb75b
Refuse to install firmware the charger does not offer
Varitras Nov 1, 2026
3d5e720
Test an unload whose disconnect fails
Varitras Nov 1, 2026
cce43b4
Let closing for good never fail its caller
Varitras Nov 1, 2026
e14cc67
Note the identity, firmware, unload and log fixes
Varitras Nov 1, 2026
042eb2c
Read the backoff reset from state, drop an orphan logger
Varitras Nov 1, 2026
73fd0b9
Read the growing backoff from state, not the clock
Varitras Nov 2, 2026
e7628f2
Test the warning budget for every repeated defect
Varitras Nov 2, 2026
27c150d
Warn once about any defect that repeats per frame
Varitras Nov 2, 2026
9e1ffbe
Let the fake client check the serial it is built with
Varitras Nov 2, 2026
7214fac
Correct two docstrings on identity and backoff
Varitras Nov 2, 2026
cd421c8
Test the review-ID guard against its old samples
Varitras Nov 2, 2026
a0b04a9
Restore the two-digit review IDs to the guard
Varitras Nov 2, 2026
c8dc7cc
Test that the fake client refuses another charger twice
Varitras Nov 2, 2026
dafab9d
Keep the fake's charger serial apart from the client's
Varitras Nov 2, 2026
17343a7
Test that a failing value costs only itself
Varitras Nov 3, 2026
6c2ce8d
Test the guard's file list, more ID shapes and the fake
Varitras Nov 3, 2026
831abc3
Make the ID guard read what it claims, teach the fake hello
Varitras Nov 3, 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
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Every text file is stored and checked out with LF. This is the native
# means for the problem a hand-written guard used to watch: a generator run
# on Windows would otherwise rewrite whole files to CRLF and bury the real
# change in the diff (audit VA-11 / ledger L-006).
# change in the diff.
* text=auto eol=lf
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ permissions:
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
Expand All @@ -39,6 +40,7 @@ jobs:
# install.
minimum-home-assistant:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
Expand All @@ -54,12 +56,14 @@ jobs:

hassfest:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: home-assistant/actions/hassfest@master

hacs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: hacs/action@main
Expand Down
19 changes: 11 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,39 @@ permissions:

jobs:
# The tag alone would publish whatever check.sh --release passes; hassfest,
# HACS and the minimum-HA run only ever saw the branch (audit A13-02).
# HACS and the minimum-HA run only ever saw the branch.
compatibility:
uses: ./.github/workflows/ci.yml

release:
needs: compatibility
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # the history main has, for the check below
# Tags come from main only. One pushed from a branch before its merge
# published code main does not contain, to every HACS user (A18-31).
# published code main does not contain, to every HACS user.
- name: Release only what main contains
run: git merge-base --is-ancestor "$GITHUB_SHA" origin/main
- uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Install dependencies
run: pip install --group dev
- name: Full gates including mutation run
run: bash scripts/check.sh --release
# Before the long gate: a wrong tag or a missing changelog section
# fails in seconds instead of after the mutation run.
- name: Verify tag matches manifest version
run: |
VERSION=$(python3 -c "import json;print(json.load(open('custom_components/wattpilot/manifest.json'))['version'])")
[ "v$VERSION" = "$GITHUB_REF_NAME" ] || { echo "tag $GITHUB_REF_NAME != manifest v$VERSION"; exit 1; }
- name: Take the release notes from the changelog
run: python3 scripts/release_notes.py "${GITHUB_REF_NAME#v}" >release-notes.md
- name: Install dependencies
run: pip install --group dev
- name: Full gates including mutation run
run: bash scripts/check.sh --release
- name: Say whether this tag is a pre-release
id: kind
# Anything that is not exactly vX.Y.Z is a candidate (v0.1.2b1), and
Expand All @@ -50,8 +55,6 @@ jobs:
echo "prerelease=true" >>"$GITHUB_OUTPUT"
fi

- name: Take the release notes from the changelog
run: python3 scripts/release_notes.py "${GITHUB_REF_NAME#v}" >release-notes.md
- name: Create GitHub release
uses: softprops/action-gh-release@v3
env:
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- Setting up or reloading the charger is a little faster: the integration
reads its description of the charger's settings once per Home Assistant
start instead of every time.
- The log is quieter. A connection, a refused password and a reconnect
attempt that stalls are logged at debug level only; what you need to act
on still reaches you as a repair notice, a password request or a setup
error.
A message from the charger that keeps failing is warned about once per
connection, with the line it failed at.
- A charger that accepts the connection but never completes it is retried
ever more slowly, as one that does not answer at all already was.

### Fixed

- A Wattpilot Home on firmware 43.5 no longer reports its ID-chip list but
Expand Down Expand Up @@ -43,6 +57,18 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- The diagnostics download also removes a Wi-Fi key sent as text, the
Wi-Fi network name in `wss`, the load balancing members and the last ID
chip read.
- An entry from the earlier cloud integration, made local with Reconfigure,
now checks that the charger at its address is still its own. It never
did, so another Wattpilot with the same password at that address would
have taken over its entities and history.
- Reauthentication and Reconfigure name a different charger at the address
as such. When its password differed too, they reported a wrong password.
- Installing a firmware version the charger does not offer, for example
through a typo in an action, is refused with an error. It was sent to the
charger as is.
- Unloading or reloading no longer fails when closing the connection
fails. The entry then refused every reload until Home Assistant
restarted.

## [0.3.0] - 2026-10-21

Expand Down
4 changes: 2 additions & 2 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Protocol knowledge and property semantics build on prior work:
which carries its MIT licence text, and is maintained here since. Only the
modules this integration uses came along (client, auth, definition, models,
exceptions, utils and the API definition resource); mqtt, shell and
discovery did not. Changes against 1.4.0 are marked in the source with a
`wattpilot:` comment.
discovery did not. It has been changed throughout since: utils was dropped,
and the connection handling moved out of the client into connection.py.
- ruaan-deysel/ha-wattpilot (Apache-2.0) — the integration this project
replaces. No source code was copied from the Apache-2.0 project; only
interoperability facts (property keys, enum values, unique-id suffixes)
Expand Down
39 changes: 18 additions & 21 deletions custom_components/wattpilot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from homeassistant import config_entries
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryError, HomeAssistantError
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect

Expand Down Expand Up @@ -86,7 +86,7 @@ def _replacements(self) -> list[tuple[str, str]]:

def _serial_masks(entries: list[ConfigEntry]) -> list[tuple[str, str]]:
# A serial is all digits; an entry awaiting it is keyed on a name or
# address, which got masked in every other title as well (R1-17).
# address, which got masked in every other title as well.
return [
(entry.unique_id, mask_serial(entry.unique_id))
for entry in entries
Expand All @@ -95,8 +95,8 @@ def _serial_masks(entries: list[ConfigEntry]) -> list[tuple[str, str]]:


def _address_title_masks(entries: list[ConfigEntry]) -> list[tuple[str, str]]:
# A fork entry titled by its address put it into every setup message
# (RF-P-02). An address literal, unlike a name, means nothing else.
# A fork entry titled by its address put it into every setup message.
# An address literal, unlike a name, means nothing else.
return [(entry.title, "<address>") for entry in entries if _is_address(entry.title)]


Expand All @@ -112,7 +112,7 @@ async def async_setup(hass: HomeAssistant, _config: ConfigType) -> bool:
"""Register integration services and mask serials in setup messages."""
async_setup_services(hass)
# Never removed: the setups the stop cancels log their title after the
# stop event, and lost the mask with it (R1-05).
# stop event, and lost the mask with it.
serial_mask = _SerialMask(hass)
for logger in _ENTRY_LOGGERS:
logger.addFilter(serial_mask)
Expand Down Expand Up @@ -149,23 +149,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: WattpilotConfigEntry) ->
# Home Assistant calls async_unload_entry only for an entry that finished
# setup, so every failure below has to hand them back itself -- otherwise
# the entry sits in SETUP_ERROR with the connection still open, and the
# next retry puts a second hub on the same dispatcher signals (audit
# VA-02, measured through the config entry manager).
# next retry puts a second hub on the same dispatcher signals (measured
# through the config entry manager).
#
# BaseException, not Exception: Home Assistant cancels a setup that runs
# too long, and CancelledError is not an Exception -- the narrower catch
# handed nothing back on that path (audit A11-05).
# handed nothing back on that path.
try:
await _finish_setup(hass, entry, hub)
except BaseException:
# A failed disconnect must not replace the error that ended setup --
# that one says what the user has to fix (audit A14-03).
try:
await hub.async_shutdown()
except HomeAssistantError:
_LOGGER.debug(
"Ignoring error while closing after failed setup", exc_info=True
)
# that one says what the user has to fix; async_shutdown keeps it out.
await hub.async_shutdown()
raise
return True

Expand All @@ -180,7 +175,7 @@ def _reload_when_firmware_changes(
their description tables at setup, and an entity freezes the device's
sw_version when it is built. So a firmware jump leaves the newly
supported entities missing and the device page showing the old version
until someone reloads by hand (audit VA-05). A reload redoes both.
until someone reloads by hand. A reload redoes both.
"""
known = hub.firmware

Expand Down Expand Up @@ -209,7 +204,9 @@ async def _finish_setup(
Split out so its caller can put a single cleanup guard around all of it.
"""
serial = hub.serial
if entry.unique_id != serial:
# Also when the serial already matches: confirming it is what ends the
# permission, or the client keeps being built without an identity check.
if entry.unique_id != serial or awaits_serial(entry):
await _adopt_serial(hass, entry, serial)
await async_migrate_legacy_unique_ids(
hass, entry, serial, hub.variant, hub.properties
Expand Down Expand Up @@ -283,7 +280,7 @@ async def _remove_ignored_card(hass: HomeAssistant, serial: str) -> None:
Drop an ignored card for the charger this entry is about to own.

It only kept the card from coming back, which the entry does now; left
in place it would share this entry's unique_id (audit A18-07).
in place it would share this entry's unique_id.
"""
for ignored in hass.config_entries.async_entries(DOMAIN):
if (
Expand All @@ -299,7 +296,7 @@ def _close_open_cards(hass: HomeAssistant, serial: str) -> None:
Close the discovery cards still offering the charger just adopted.

One opened before the entry knew its serial stays up otherwise, and
ignoring it makes Home Assistant replace the entry (audit A18-01).
ignoring it makes Home Assistant replace the entry.
"""
flows = hass.config_entries.flow
for card in flows.async_progress_by_handler(
Expand Down Expand Up @@ -353,8 +350,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
CONF_AWAITING_SERIAL: True,
}
# Without a name the device took the host name, and every entity the
# fork lacked got the serial in its ID (RF-P-04); a cloud entry too,
# once Reconfigure made it local (PC5-R-04).
# fork lacked got the serial in its ID; a cloud entry too,
# once Reconfigure made it local.
new_data[CONF_NAME] = new_device_name(
hass, preferred=old.get("friendly_name"), exclude=entry.entry_id
)
Expand Down
4 changes: 2 additions & 2 deletions custom_components/wattpilot/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
integration uses were taken over; mqtt, shell and discovery were not, and
with them neither aiomqtt, prompt-toolkit nor pydantic.

Changes against 1.4.0 carry a `wattpilot:` comment naming the finding, so
the adopted parts stay distinguishable from ours.
It has been changed throughout since, so the changes are no longer marked
one by one.
"""

from .client import Wattpilot, mask_serial
Expand Down
Loading
Loading