Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.2 KiB
AGENTS.md
Purpose
Groundwork for a Home Assistant integration for West Wood Club (an Irish gym
chain). The West Wood app is a white-label build of PerfectGym Go, so the
integration targets PerfectGym's hosted backend at https://goapi2.perfectgym.com.
The repo holds the reverse-engineering capture, the API docs derived from it, a token helper, and the Home Assistant custom component itself (see Integration).
Layout
api.md— documented PerfectGym endpoints (login, club list, live occupancy), secrets redacted. The source of truth for API behaviour; extend it as more endpoints are mapped.get-token.py— logs in and prints a bearer token to stdout (stdlib only).custom_components/west_wood_club/— the Home Assistant integration.android-flows.mitm— mitmproxy capture of the app's traffic. Gitignored and untracked: it contains real credentials and a bearer token in cleartext. Never commit it or copy its secrets into tracked files.token.txt— a captured/extracted bearer token. Also gitignored.flake.nix/.envrc— Nix dev shell (providesmitmproxy).
Dev environment
Nix flake + nix-direnv. direnv allow enters a shell with mitmproxy/python.
To run Python/mitmproxy from a non-interactive shell (where direnv does not
auto-activate and there's no python on PATH), use direnv exec .:
direnv exec . python script.py
This reuses nix-direnv's cached dev shell (~0.06s). Prefer it over
nix develop --command ..., which re-evaluates the flake every call (slow, and
noisy with "Git tree is dirty" warnings).
Gotchas:
- Nix flakes only see git-tracked files.
git addnew files beforenix develop/nix flake lock, or Nix errors with "not tracked by Git". - Use absolute paths when a script opens
android-flows.mitm—nix develop --commandresets cwd to the repo root (direnv exec .keeps the current dir).
Code style
Python uses single-quoted strings ('...'). Reformat with
ruff format --config "format.quote-style='single'" <paths> (ruff is available via
nix run nixpkgs#ruff). Docstrings stay triple-double-quoted (""").
In prose (commit messages, docs, comments), backtick-quote anything code-like — paths, filenames, identifiers, commands, endpoints — rather than plain or double-quoted text.
Working with the capture
Read flows with the mitmproxy Python API:
from mitmproxy.io import FlowReader
from mitmproxy.http import HTTPFlow
with open("/abs/path/android-flows.mitm", "rb") as f:
for flow in FlowReader(f).stream():
if isinstance(flow, HTTPFlow) and "perfectgym.com" in flow.request.host:
... # flow.request / flow.response
When dumping flows, redact Authorization / Cookie headers and the login body
(email + password) before writing anything to a tracked file.
API essentials
Full detail in api.md. Quick reference:
- Responses are wrapped
{ "data": ..., "errors": ... };errorsisnullon success. - Auth:
POST /v1/Authorize/LogInWithEmail(white-label ID goes in the body) → reuse the returnedbearer <token>as theAuthorizationheader. Token expiry is unconfirmed (expireTimewasnull). - Authenticated endpoints need only the
Authorizationheader — theX-Go-*headers and appUser-Agentthe app sends are not required (verified against the clubs endpoint). - Live occupancy:
GET /v1/Clubs/WhoIsInCount→countperclubId(the main sensor signal).clubIdmaps toidfromGET /v1/Clubs/Clubs.
Integration
custom_components/west_wood_club/ is a UI-configured (config-flow) integration.
- Auth model: the user pastes a long-lived bearer token (from
get-token.py); no credentials are stored. A rejected token (WestWoodAuthError→ coordinator raisesConfigEntryAuthFailed) triggers HA's reauth flow to paste a fresh one. - One device, N sensors: a single
DataUpdateCoordinatorpollsWhoIsInCountonce per interval for all clubs; oneSensorEntityper selected club reads itsclub_idout ofcoordinator.data. All sensors share one device (identifiers={(DOMAIN, entry.entry_id)}) named "West Wood Club". - Config flow steps:
userandreauthare HA-fixed names (dispatched by the flow source);clubsandreauth_confirmare reached because a form was shown with thatstep_id(HA callsasync_step_<step_id>on submit). Every step name must also have a matching key underconfig.stepinstrings.json. - Nix:
flake.nixexposespackages.west_wood_club(built withbuildHomeAssistantComponent) andoverlays.default, which adds it topkgs.home-assistant-custom-components. On a NixOS host, apply the overlay and list it inservices.home-assistant.customComponents; it must be built against the same Python as the host'shome-assistant. Bumpversionin bothmanifest.jsonand the flake on code changes. The integration has no externalrequirements, so no extra Nix packaging is needed.
Security
The capture and token.txt hold live credentials/tokens. Keep them gitignored,
and prefer the WESTWOOD_EMAIL / WESTWOOD_PASSWORD env vars (read by
get-token.py) over hardcoding credentials anywhere.