Skip to content

Commit fb93d59

Browse files
committed
chore: add CLAUDE.md with project guidance for Claude Code
1 parent 9a52259 commit fb93d59

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Branching strategy
6+
7+
- `main` — stable releases only
8+
- `develop` — beta/integration branch; the default merge target for all feature and bugfix work
9+
10+
**Always branch from `develop` for features and bugfixes.** PRs must target `develop`, not `main`. The only exception is a hotfix that must go directly to `main`.
11+
12+
## Commands
13+
14+
```bash
15+
# Install dependencies (first time or after lockfile changes)
16+
poetry install --no-root
17+
18+
# Type check
19+
poetry run mypy
20+
21+
# Lint (ruff runs with --fix --unsafe-fixes in pre-commit)
22+
poetry run ruff check .
23+
poetry run ruff format .
24+
25+
# Run all tests with coverage
26+
poetry run pytest tests --cov
27+
28+
# Run a single test file or test
29+
poetry run pytest tests/test_vehicle_info.py
30+
poetry run pytest tests/test_vehicle_info.py::TestMg4UrbanRealBatteryCapacity::test_standard_range_43kwh -v
31+
```
32+
33+
Pre-commit hooks run `ruff`, `ruff-format`, `mypy`, and `poetry-check` on every commit. Pytest runs as a **pre-push** hook. Always run mypy and ruff before committing to avoid fixup commits.
34+
35+
## Architecture
36+
37+
### Data flow
38+
39+
The gateway polls the SAIC cloud API on a per-vehicle schedule and bridges results to an MQTT broker. Incoming MQTT `/set` commands are forwarded back to the SAIC API.
40+
41+
```
42+
SAIC Cloud API
43+
↓ (VehicleState.should_refresh() controls timing)
44+
VehicleHandler.__polling()
45+
46+
VehicleState.handle_vehicle_status() → VehicleStatusRespPublisher → MQTT
47+
VehicleState.handle_charge_status() → ChrgMgmtDataRespPublisher → MQTT
48+
49+
extractors.extract_soc/range() (cross-fuses BMS + vehicle status values)
50+
51+
AbrpApi / OsmAndApi / OpenWBIntegration (optional side-effects)
52+
53+
MQTT broker (/set topics)
54+
55+
MqttGateway → VehicleHandler → VehicleCommandHandler → SAIC API
56+
```
57+
58+
### Key modules
59+
60+
**`src/mqtt_gateway.py`** — top-level orchestrator. Implements `MqttCommandListener` (MQTT callbacks) and `VehicleHandlerLocator` (VIN → handler lookup).
61+
62+
**`src/vehicle.py``VehicleState`** — the polling state machine. Controls refresh timing via `PollingPhase` and `RefreshMode` enums. Exponential backoff on errors (doubles up to `refresh_period_inactive`). Polling is gated by `is_complete()` — all four refresh periods must be populated before the first poll. They are restored from retained MQTT messages on reconnect or defaulted by `configure_missing()` after a 10-second startup delay.
63+
64+
**`src/handlers/vehicle.py``VehicleHandler`** — per-VIN lifecycle. Owns `VehicleState`, `VehicleCommandHandler`, all integrations, and HA discovery. The `handle_vehicle()` coroutine is a long-lived asyncio task.
65+
66+
**`src/vehicle_info.py``VehicleInfo`** — static metadata derived from `VinInfo`. Holds series/model identity, vehicle configuration properties (e.g. `BType` for NMC/LFP battery type), battery capacity lookup, AC temperature mapping, and feature flags (`is_ev`, `has_sunroof`, etc.). `is_ev` is determined by series **not** starting with `"ZP22"`.
67+
68+
**`src/publisher/core.py``Publisher`** — abstract base with typed publish methods. Handles topic sanitization, data anonymization, and LWT. The `publish(key, Publishable)` dispatcher checks `bool` before `int` (Python's `isinstance(True, int)` is `True`).
69+
70+
**`src/handlers/command/`** — one `CommandHandlerBase` subclass per writable MQTT topic. All registered in `handlers/command/__init__.py::ALL_COMMAND_HANDLERS`.
71+
72+
**`src/status_publisher/`** — stateless publishers for each API response type. Return frozen dataclasses that carry extracted values back up to `VehicleState` for cross-cutting decisions (e.g. BMS vs vehicle SoC reconciliation).
73+
74+
**`src/extractors/__init__.py`** — pure functions that reconcile values present in both API responses. BMS values take precedence over vehicle status.
75+
76+
### Battery capacity (`src/vehicle_info.py`)
77+
78+
`real_battery_capacity` dispatches by `series` prefix to a vehicle-specific property. When adding a new model, add an `elif self.series.startswith(...)` branch and a corresponding `__<model>_real_battery_capacity` property. `supports_target_soc` (`BType == "1"`) distinguishes NMC from LFP where both share a series prefix. Custom capacity via `BATTERY_CAPACITY_MAPPING` (`VIN=kWh`) always overrides the lookup.
79+
80+
### Integrations (`src/integrations/`)
81+
82+
All optional, instantiated per-VIN:
83+
- **Home Assistant**: MQTT auto-discovery. Re-published on broker reconnect or HA `online` LWT.
84+
- **OpenWB**: subscribes to charger MQTT topics; triggers forced vehicle refresh on charge start; publishes SoC/range back to the charger.
85+
- **ABRP / OsmAnd**: REST/HTTP telemetry push after each successful poll.
86+
87+
### MQTT topic structure
88+
89+
```
90+
<prefix>/<saic_user>/vehicles/<vin>/<domain>/<key> # status
91+
<prefix>/<saic_user>/vehicles/<vin>/<domain>/<key>/set # writable
92+
<prefix>/<saic_user>/account/... # account-level
93+
<prefix>/_internal/api/... # raw API debug
94+
```
95+
96+
All topic constants are in `src/mqtt_topics.py`.

0 commit comments

Comments
 (0)