|
1 | | -# GitHub Copilot Instructions for python-bsblan |
| 1 | +# Canonical Instructions |
2 | 2 |
|
3 | | -This repository contains the `python-bsblan` library, an asynchronous Python client for BSB-LAN devices (heating controllers). |
| 3 | +The canonical instruction file for this repository is: |
4 | 4 |
|
5 | | -## Project Overview |
| 5 | +- `AGENTS.md` |
6 | 6 |
|
7 | | -- **Language**: Python 3.12+ |
8 | | -- **Type**: Async library using `aiohttp` |
9 | | -- **Purpose**: Communicate with BSB-LAN devices to read/write heating parameters |
10 | | -- **License**: MIT |
11 | | - |
12 | | -## Code Quality Standards |
13 | | - |
14 | | -### Required Before Committing |
15 | | - |
16 | | -Always run these commands after making changes: |
17 | | - |
18 | | -```bash |
19 | | -# Run all prek hooks (ruff, ty, pylint) |
20 | | -uv run prek run --all-files |
21 | | -``` |
22 | | - |
23 | | -### Prek Includes |
24 | | -- **Ruff**: Linting and formatting (88 char line limit) |
25 | | -- **ty**: Static type checking |
26 | | -- **Pylint**: Code analysis |
27 | | - |
28 | | -### Coverage Requirements |
29 | | -- Maintain **95%+ total test coverage** |
30 | | -- **Patch coverage must be 100%** - all new/modified code must be fully tested |
31 | | -- GitHub Actions will fail if patch coverage is below 100% |
32 | | -- Run coverage check: `uv run pytest --cov=src/bsblan --cov-report=term-missing` |
33 | | - |
34 | | -## Project Structure |
35 | | - |
36 | | -``` |
37 | | -src/bsblan/ |
38 | | -├── __init__.py # Package exports |
39 | | -├── bsblan.py # Main BSBLAN client class |
40 | | -├── constants.py # Parameter IDs and mappings |
41 | | -├── models.py # Dataclass models for API responses |
42 | | -├── utility.py # Helper utilities |
43 | | -├── exceptions.py # Custom exceptions |
44 | | -└── py.typed # PEP-561 marker |
45 | | -
|
46 | | -tests/ |
47 | | -├── conftest.py # Pytest fixtures |
48 | | -├── fixtures/ # JSON test data |
49 | | -└── test_*.py # Test files |
50 | | -``` |
51 | | - |
52 | | -## Parameter Naming Conventions |
53 | | - |
54 | | -### BSB-LAN Parameters |
55 | | -Parameters are identified by numeric IDs and mapped to readable names in `constants.py`. |
56 | | - |
57 | | -**Naming Rules:** |
58 | | -- Use `snake_case` for all parameter names |
59 | | -- Group related parameters with common prefixes |
60 | | -- Legionella-related parameters use `legionella_function_*` prefix: |
61 | | - - `legionella_function_setpoint` (ID: 1645) |
62 | | - - `legionella_function_periodicity` (ID: 1641) |
63 | | - - `legionella_function_day` (ID: 1642) |
64 | | - - `legionella_function_time` (ID: 1644) |
65 | | - - `legionella_function_dwelling_time` (ID: 1646) |
66 | | -- DHW (Domestic Hot Water) parameters use `dhw_*` prefix |
67 | | - |
68 | | -### Adding New Parameters |
69 | | - |
70 | | -1. **Add to `constants.py`**: |
71 | | - ```python |
72 | | - BASE_HOT_WATER_PARAMS: Final[dict[str, str]] = { |
73 | | - "1645": "legionella_function_setpoint", # Parameter ID: name |
74 | | - } |
75 | | - ``` |
76 | | - |
77 | | -2. **Add to model in `models.py`**: |
78 | | - ```python |
79 | | - class HotWaterConfig(BaseModel): |
80 | | - legionella_function_setpoint: EntityInfo[float] | None = None |
81 | | - ``` |
82 | | - |
83 | | -3. **Update method in `bsblan.py`** if the parameter is settable: |
84 | | - ```python |
85 | | - async def set_hot_water( |
86 | | - self, |
87 | | - params: SetHotWaterParam, |
88 | | - ) -> None: |
89 | | - ``` |
90 | | - And add the field to `SetHotWaterParam` in `models.py`: |
91 | | - ```python |
92 | | - @dataclass |
93 | | - class SetHotWaterParam: |
94 | | - legionella_function_setpoint: float | None = None |
95 | | - ``` |
96 | | - |
97 | | -4. **Add tests in `tests/test_*.py`** |
98 | | - |
99 | | -## Polling Categories |
100 | | - |
101 | | -Parameters are organized into polling categories based on how frequently they change: |
102 | | - |
103 | | -### Fast Poll (State - every update) |
104 | | -- Current temperatures |
105 | | -- HVAC action/state |
106 | | -- Pump states |
107 | | - |
108 | | -### Slow Poll (Config - every 5 minutes) |
109 | | -- Operating modes |
110 | | -- Setpoints |
111 | | -- Legionella function settings |
112 | | -- Time programs |
113 | | - |
114 | | -### Static (rarely changes) |
115 | | -- Device identification |
116 | | -- Min/max temperature limits |
117 | | - |
118 | | -## Hot Water Parameter Groups |
119 | | - |
120 | | -Hot water parameters are split into groups for granular lazy loading: |
121 | | - |
122 | | -| Group | Params | Method | Use Case | |
123 | | -|-------|--------|--------|----------| |
124 | | -| essential | 5 | `hot_water_state()` | Frequent polling | |
125 | | -| config | 16 | `hot_water_config()` | Advanced settings | |
126 | | -| schedule | 8 | `hot_water_schedule()` | Time programs | |
127 | | - |
128 | | -Defined in `constants.py`: |
129 | | -- `HOT_WATER_ESSENTIAL_PARAMS` - operating_mode, nominal_setpoint, etc. |
130 | | -- `HOT_WATER_CONFIG_PARAMS` - legionella settings, eco mode, etc. |
131 | | -- `HOT_WATER_SCHEDULE_PARAMS` - daily time programs |
132 | | - |
133 | | -## Data Models |
134 | | - |
135 | | -### Model Pattern |
136 | | -All models use `pydantic` `BaseModel` for validation and serialization: |
137 | | - |
138 | | -```python |
139 | | -from pydantic import BaseModel |
140 | | - |
141 | | -class HotWaterConfig(BaseModel): |
142 | | - """Hot water configuration parameters.""" |
143 | | - operating_mode: EntityInfo[int] | None = None |
144 | | - nominal_setpoint: EntityInfo[float] | None = None |
145 | | -``` |
146 | | - |
147 | | -### EntityInfo Structure |
148 | | -Each parameter returns an `EntityInfo[T]` (generic `BaseModel`) with: |
149 | | -- `value`: The actual value (typed via generic `T`) |
150 | | -- `unit`: Unit of measurement |
151 | | -- `desc`: Human-readable description |
152 | | -- `data_type`: Data type information |
153 | | - |
154 | | -## Async Patterns |
155 | | - |
156 | | -### Client Usage |
157 | | -```python |
158 | | -from bsblan import BSBLAN, BSBLANConfig, SetHotWaterParam |
159 | | - |
160 | | -async with BSBLAN(BSBLANConfig(host="192.168.1.100")) as client: |
161 | | - state = await client.state() |
162 | | - await client.set_hot_water(SetHotWaterParam(nominal_setpoint=55.0)) |
163 | | -``` |
164 | | - |
165 | | -### Lazy Loading Architecture |
166 | | -The library uses lazy loading for optimal performance: |
167 | | -- **Initialization**: Only fetches firmware version (fast startup) |
168 | | -- **Section validation**: Deferred until section is first accessed |
169 | | -- **Hot water granular loading**: Each method validates only its param group |
170 | | -- **Race condition prevention**: Per-section/group asyncio locks |
171 | | - |
172 | | -```python |
173 | | -# Initialize() is fast - only fetches firmware |
174 | | -await client.initialize() # ~0.02s |
175 | | - |
176 | | -# Section validated on first access |
177 | | -await client.state() # Validates heating section on first call |
178 | | - |
179 | | -# Hot water methods validate only their param groups: |
180 | | -await client.hot_water_state() # 5 essential params only |
181 | | -await client.hot_water_config() # 16 config params only |
182 | | -await client.hot_water_schedule() # 8 schedule params only |
183 | | -``` |
184 | | - |
185 | | -### Concurrency & Locking |
186 | | -The library uses asyncio locks to prevent race conditions during lazy loading: |
187 | | -- `_section_locks`: Per-section locks (heating, sensor, etc.) |
188 | | -- `_hot_water_group_locks`: Per-group locks (essential, config, schedule) |
189 | | - |
190 | | -Double-checked locking pattern: |
191 | | -1. Fast path: Check if validated (no lock) |
192 | | -2. Acquire lock for specific section/group |
193 | | -3. Double-check after acquiring lock |
194 | | -4. Perform validation inside the lock |
195 | | - |
196 | | -This prevents duplicate network requests when concurrent calls access the same section before validation completes. |
197 | | - |
198 | | -### Error Handling |
199 | | -- Use `BSBLANError` for general errors |
200 | | -- Use `BSBLANConnectionError` for connection issues |
201 | | -- Always validate only one parameter is set per API call |
202 | | - |
203 | | -## Testing Patterns |
204 | | - |
205 | | -### Test Structure |
206 | | -```python |
207 | | -@pytest.mark.asyncio |
208 | | -async def test_set_hot_water(mock_bsblan: BSBLAN) -> None: |
209 | | - """Test setting BSBLAN hot water state.""" |
210 | | - await mock_bsblan.set_hot_water( |
211 | | - SetHotWaterParam(nominal_setpoint=60.0) |
212 | | - ) |
213 | | - mock_bsblan._request.assert_awaited_with( |
214 | | - base_path="/JS", |
215 | | - data={"Parameter": "1610", "Value": "60.0", "Type": "1"}, |
216 | | - ) |
217 | | -``` |
218 | | - |
219 | | -### Fixtures Location |
220 | | -Test fixtures (JSON responses) are in `tests/fixtures/` |
221 | | - |
222 | | -## Common Tasks |
223 | | - |
224 | | -### Fetching Parameters from a Real Device |
225 | | - |
226 | | -Use `examples/fetch_param.py` to query raw parameter data from a real BSB-LAN device before adding new parameters: |
227 | | - |
228 | | -```bash |
229 | | -# Set your device connection details |
230 | | -# Leave BSBLAN_HOST unset to use auto-discovery, or set it explicitly: |
231 | | -export BSBLAN_HOST="192.168.1.100" |
232 | | -export BSBLAN_PASSKEY=your_passkey # if needed |
233 | | - |
234 | | -# Fetch one or more parameters |
235 | | -cd examples && python fetch_param.py 1645 |
236 | | -cd examples && python fetch_param.py 1645 1641 1642 1644 1646 |
237 | | -``` |
238 | | - |
239 | | -The output shows the raw JSON response including value, unit, description, and data type — use this to determine the correct model field type and naming. |
240 | | - |
241 | | -### Adding a New Settable Parameter |
242 | | - |
243 | | -0. Fetch the parameter from a real device using `examples/fetch_param.py` to inspect the raw response |
244 | | -1. Add parameter ID mapping in `constants.py` |
245 | | -2. Add field to appropriate model in `models.py` |
246 | | -3. Add parameter to method signature in `bsblan.py` |
247 | | -4. Update docstring with parameter description |
248 | | -5. Add state preparation logic in `_prepare_*_state()` method |
249 | | -6. Add tests for the new parameter |
250 | | -7. Run `uv run prek run --all-files` |
251 | | - |
252 | | -### Renaming a Parameter |
253 | | - |
254 | | -When renaming parameters for consistency: |
255 | | -1. Update `constants.py` - parameter mapping |
256 | | -2. Update `models.py` - dataclass field |
257 | | -3. Update `bsblan.py` - method parameters and state handling |
258 | | -4. Update `tests/` - all test files using the parameter |
259 | | -5. Update `examples/` - any example code |
260 | | -6. Run `uv run prek run --all-files` |
261 | | - |
262 | | -## API Versions |
263 | | - |
264 | | -The library supports BSB-LAN API versions: |
265 | | -- **v1**: Original API |
266 | | -- **v3**: Extended API with additional parameters |
267 | | - |
268 | | -Version-specific parameters are handled in `constants.py` with extension dictionaries. |
269 | | - |
270 | | -## Don't Forget |
271 | | - |
272 | | -- ✅ Run `uv run prek run --all-files` after every change |
273 | | -- ✅ Maintain 95%+ test coverage |
274 | | -- ✅ Use type hints on all functions |
275 | | -- ✅ Add docstrings to public methods |
276 | | -- ✅ Keep line length under 88 characters |
277 | | -- ✅ Use consistent parameter naming (check existing patterns) |
| 7 | +Keep this file as a lightweight compatibility pointer to avoid duplicated guidance. |
0 commit comments