Add the write-action layer and a daily upstream compatibility check
Upstream compatibility / compat (push) Failing after 7s
Validate / hassfest (push) Failing after 25s
Validate / HACS (push) Failing after 17s
Validate / pytest (push) Successful in 9s

actions.py covers skip, donate, cart add/remove, subscriptions and vacation
holds. Every mutating endpoint on the site is guarded by rotating per-render
tokens, so each action re-derives them from a live page rather than storing
anything; skip additionally compares the date the server states in its
confirmation against the date it was asked to skip, and refuses on a mismatch.
All actions default to dry_run.

tools/compat.py records what the integration assumes about a site that offers
no API and no stability contract, and CI asserts it daily. Only the
unauthenticated surface is covered: checking the rest would mean putting a
personal account password in public repo secrets.
This commit is contained in:
flan
2026-08-03 20:02:58 +00:00
parent 3e880ff2c1
commit 213c16d98e
6 changed files with 776 additions and 6 deletions
+13 -6
View File
@@ -362,20 +362,27 @@ class FreshHarvestClient:
"""
return "sign out" in body.lower()
async def async_get_snapshot(self) -> AccountSnapshot:
"""Fetch and parse the dashboard, re-authenticating once if needed."""
async def async_fetch(self, path: str) -> str:
"""Fetch any portal page, re-authenticating once if the session lapsed.
The shared primitive for reads and for the token-scraping that every
write action in `actions.py` has to do first.
"""
if not self._authenticated:
await self.async_login()
try:
body = await self._get(DASHBOARD)
body = await self._get(path)
if not self._signed_in(body):
self._authenticated = False
await self.async_login()
body = await self._get(DASHBOARD)
body = await self._get(path)
if not self._signed_in(body):
raise FreshHarvestAuthError("could not hold a signed-in session")
except aiohttp.ClientError as err:
raise FreshHarvestError(f"dashboard request failed: {err}") from err
raise FreshHarvestError(f"request for {path} failed: {err}") from err
return body
return parse_dashboard(body)
async def async_get_snapshot(self) -> AccountSnapshot:
"""Fetch and parse the dashboard."""
return parse_dashboard(await self.async_fetch(DASHBOARD))