Skip to content
This repository was archived by the owner on Apr 12, 2026. It is now read-only.

Commit cb66975

Browse files
Ubuntuclaude
authored andcommitted
docs(quick-260409-m53): parameter backup button + viewer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent df0995f commit cb66975

3 files changed

Lines changed: 477 additions & 2 deletions

File tree

.planning/STATE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ None.
7373
| # | Description | Date | Commit | Directory |
7474
|---|-------------|------|--------|-----------|
7575
| 260409-l1l | Add Options Flow to reconfigure IP address and host settings in HA | 2026-04-09 | da8f155 | [260409-l1l-add-options-flow-to-reconfigure-ip-addre](./quick/260409-l1l-add-options-flow-to-reconfigure-ip-addre/) |
76+
| 260409-m53 | Add parameter backup button and backup viewer to HA integration | 2026-04-09 | df0995f | [260409-m53-add-parameter-backup-button-and-backup-v](./quick/260409-m53-add-parameter-backup-button-and-backup-v/) |
7677

7778
## Session Continuity
7879

7980
Last session: 2026-04-09
80-
Stopped at: Quick task 260409-l1l completed
81-
Resume file: .planning/quick/260409-l1l-add-options-flow-to-reconfigure-ip-addre/260409-l1l-SUMMARY.md
81+
Stopped at: Quick task 260409-m53 completed
82+
Resume file: .planning/quick/260409-m53-add-parameter-backup-button-and-backup-v/260409-m53-SUMMARY.md
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
---
2+
phase: quick
3+
plan: 260409-m53
4+
type: execute
5+
wave: 1
6+
depends_on: []
7+
files_modified:
8+
- custom_components/luxtronik2_modbus_proxy/button.py
9+
- custom_components/luxtronik2_modbus_proxy/sensor.py
10+
- custom_components/luxtronik2_modbus_proxy/__init__.py
11+
- custom_components/luxtronik2_modbus_proxy/const.py
12+
- custom_components/luxtronik2_modbus_proxy/strings.json
13+
- custom_components/luxtronik2_modbus_proxy/translations/en.json
14+
- custom_components/luxtronik2_modbus_proxy/translations/de.json
15+
autonomous: true
16+
must_haves:
17+
truths:
18+
- "A 'Backup Parameters' button entity appears on the device page under Configuration"
19+
- "Pressing the button reads all parameters and saves a timestamped JSON file"
20+
- "JSON file is saved to /config/luxtronik2_backups/ with YYYY-MM-DD_HH-MM-SS.json naming"
21+
- "A persistent_notification fires after backup with summary (count, filename, timestamp)"
22+
- "A sensor entity shows the last backup timestamp and filename"
23+
artifacts:
24+
- path: "custom_components/luxtronik2_modbus_proxy/button.py"
25+
provides: "Button entity + backup logic"
26+
- path: "custom_components/luxtronik2_modbus_proxy/sensor.py"
27+
provides: "Last backup sensor entity (added to existing sensor platform)"
28+
- path: "custom_components/luxtronik2_modbus_proxy/__init__.py"
29+
provides: "button platform registration in PLATFORMS"
30+
key_links:
31+
- from: "button.py"
32+
to: "coordinator"
33+
via: "coordinator._sync_read pattern to get full parameter data with names/types"
34+
- from: "button.py"
35+
to: "hass.components.persistent_notification"
36+
via: "async_create after backup"
37+
- from: "button.py"
38+
to: "sensor.py last_backup sensor"
39+
via: "hass.data storage of last backup metadata"
40+
---
41+
42+
<objective>
43+
Add a "Backup Parameters" button entity that reads all 1,126 parameters from the
44+
Luxtronik controller and saves them as a timestamped JSON file, plus a sensor showing
45+
the last backup timestamp.
46+
47+
Purpose: Allow users to create a snapshot of all heat pump parameters before making
48+
configuration changes, providing a safety net for experimentation.
49+
50+
Output: button.py (new), updated sensor.py, updated __init__.py, updated translations.
51+
</objective>
52+
53+
<execution_context>
54+
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
55+
@$HOME/.claude/get-shit-done/templates/summary.md
56+
</execution_context>
57+
58+
<context>
59+
@custom_components/luxtronik2_modbus_proxy/__init__.py
60+
@custom_components/luxtronik2_modbus_proxy/coordinator.py
61+
@custom_components/luxtronik2_modbus_proxy/const.py
62+
@custom_components/luxtronik2_modbus_proxy/sensor.py
63+
@custom_components/luxtronik2_modbus_proxy/strings.json
64+
@custom_components/luxtronik2_modbus_proxy/translations/en.json
65+
@custom_components/luxtronik2_modbus_proxy/translations/de.json
66+
67+
<interfaces>
68+
<!-- Key types and contracts from existing codebase -->
69+
70+
From coordinator.py:
71+
```python
72+
class LuxtronikCoordinator(DataUpdateCoordinator[dict]):
73+
_host: str
74+
_port: int
75+
_lock: asyncio.Lock
76+
# coordinator.data = {"parameters": dict[int, int], "calculations": dict[int, int]}
77+
```
78+
79+
From const.py:
80+
```python
81+
DOMAIN = "luxtronik2_modbus_proxy"
82+
MANUFACTURER = "Alpha Innotec / Novelan"
83+
MODEL = "Luxtronik 2.0"
84+
DEFAULT_PORT = 8889
85+
```
86+
87+
From sensor.py (entity pattern):
88+
```python
89+
# DeviceInfo imported from homeassistant.helpers.entity (NOT device_registry)
90+
from homeassistant.helpers.entity import DeviceInfo
91+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
92+
93+
# Device info pattern used by all entities:
94+
DeviceInfo(
95+
identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)},
96+
name=MODEL, manufacturer=MANUFACTURER, model=MODEL,
97+
)
98+
```
99+
100+
From luxtronik library (used directly, NOT proxy register_definitions):
101+
```python
102+
import luxtronik
103+
import luxtronik.parameters as _lp
104+
# _lp.Parameters().parameters -> dict[int, TypedParam]
105+
# Each TypedParam has: .name (str), .value, .from_heatpump(raw_int), .to_heatpump(value)
106+
# type(param_obj).__name__ gives the datatype name (e.g., "Celsius", "HeatingMode")
107+
```
108+
</interfaces>
109+
</context>
110+
111+
<tasks>
112+
113+
<task type="auto">
114+
<name>Task 1: Create button.py with backup logic, add constants, register platform</name>
115+
<files>
116+
custom_components/luxtronik2_modbus_proxy/button.py,
117+
custom_components/luxtronik2_modbus_proxy/__init__.py,
118+
custom_components/luxtronik2_modbus_proxy/const.py
119+
</files>
120+
<action>
121+
**const.py** — Add constant:
122+
```python
123+
BACKUP_DIR = "luxtronik2_backups"
124+
```
125+
126+
**__init__.py** — Add `"button"` to the PLATFORMS list (line 29):
127+
```python
128+
PLATFORMS: list[str] = ["sensor", "select", "number", "button"]
129+
```
130+
131+
**button.py** — Create new file implementing the backup button entity.
132+
133+
Structure:
134+
1. Import `ButtonEntity` from `homeassistant.components.button`.
135+
2. Import `DeviceInfo` from `homeassistant.helpers.entity` (NOT device_registry).
136+
3. Import `luxtronik` and `luxtronik.parameters as _lp` for parameter name/type extraction.
137+
4. Import `json`, `os`, `datetime` from stdlib.
138+
139+
Class `LuxtronikBackupButton(ButtonEntity)`:
140+
- `_attr_name = "Luxtronik Backup Parameters"`
141+
- `_attr_icon = "mdi:content-save-all"`
142+
- `_attr_entity_category = EntityCategory.CONFIG` (appears under Configuration)
143+
- `_attr_unique_id = f"{entry.entry_id}_backup_parameters"`
144+
- `device_info` property: same DeviceInfo pattern as sensor.py (identifiers, name, manufacturer, model)
145+
- Store `coordinator` and `hass` references in `__init__`.
146+
147+
`async_press()` method:
148+
1. Acquire coordinator._lock (ARCH-03 — serialize with reads/writes).
149+
2. Inside lock, call `await self.hass.async_add_executor_job(self._sync_backup)`.
150+
3. After lock release, store backup metadata in `hass.data[DOMAIN][f"{entry_id}_last_backup"]` as dict with `timestamp` (ISO string) and `filename`.
151+
4. Fire `async_dispatcher_send(hass, f"{DOMAIN}_backup_complete", metadata)` so the sensor updates.
152+
5. Call `hass.components.persistent_notification.async_create(message, title, notification_id)`:
153+
- title: "Luxtronik Parameter Backup Complete"
154+
- message: f"Backed up {count} parameters to `{filename}`\nTimestamp: {timestamp}"
155+
- notification_id: f"{DOMAIN}_backup"
156+
157+
`_sync_backup()` method (runs in executor, blocking I/O OK):
158+
1. Create a fresh `luxtronik.Luxtronik` instance using the `__new__` pattern from coordinator._sync_read:
159+
```python
160+
lux = luxtronik.Luxtronik.__new__(luxtronik.Luxtronik)
161+
lux._host = self._host
162+
lux._port = self._port
163+
lux._socket = None
164+
lux.calculations = luxtronik.Calculations()
165+
lux.parameters = luxtronik.Parameters()
166+
lux.visibilities = luxtronik.Visibilities()
167+
lux.read()
168+
```
169+
2. Build the JSON structure by iterating `lux.parameters.parameters`:
170+
```python
171+
params_dict = {}
172+
for idx, param_obj in lux.parameters.parameters.items():
173+
if param_obj is None:
174+
continue
175+
entry = {
176+
"name": getattr(param_obj, "name", f"Parameter {idx}"),
177+
"type": type(param_obj).__name__,
178+
}
179+
if hasattr(param_obj, "to_heatpump") and param_obj.value is not None:
180+
try:
181+
entry["raw_value"] = int(param_obj.to_heatpump(param_obj.value))
182+
except (TypeError, ValueError, AttributeError):
183+
entry["raw_value"] = None
184+
else:
185+
entry["raw_value"] = None
186+
entry["display_value"] = str(param_obj.value) if param_obj.value is not None else None
187+
params_dict[str(idx)] = entry
188+
```
189+
3. Build the full backup dict:
190+
```python
191+
now = datetime.datetime.now(datetime.timezone.utc)
192+
filename = now.strftime("%Y-%m-%d_%H-%M-%S") + ".json"
193+
backup = {
194+
"timestamp": now.isoformat(),
195+
"host": self._host,
196+
"parameters": params_dict,
197+
}
198+
```
199+
4. Write to `{hass.config.path(BACKUP_DIR)}/{filename}`:
200+
- Create directory with `os.makedirs(backup_path, exist_ok=True)`.
201+
- Write JSON with `json.dump(backup, f, indent=2, ensure_ascii=False)`.
202+
5. Return `(filename, now.isoformat(), len(params_dict))` tuple for the caller.
203+
204+
`async_setup_entry()` function (platform setup):
205+
- Get coordinator from `hass.data[DOMAIN][entry.entry_id]`.
206+
- Create one `LuxtronikBackupButton` instance.
207+
- Call `async_add_entities([button])`.
208+
209+
IMPORTANT: Use the luxtronik library objects directly for reading parameter data (NOT the proxy package register_definitions). The coordinator pattern with `__new__` is proven and documented in coordinator.py.
210+
</action>
211+
<verify>
212+
<automated>cd /home/dwolbeck/claude-code/PUBLIC-luxtronik2-modbus-proxy && python -c "
213+
import ast, sys
214+
# Verify button.py parses
215+
with open('custom_components/luxtronik2_modbus_proxy/button.py') as f:
216+
tree = ast.parse(f.read())
217+
classes = [n.name for n in ast.walk(tree) if isinstance(n, ast.ClassDef)]
218+
assert 'LuxtronikBackupButton' in classes, f'Missing class, found: {classes}'
219+
# Verify PLATFORMS includes button
220+
with open('custom_components/luxtronik2_modbus_proxy/__init__.py') as f:
221+
src = f.read()
222+
assert '\"button\"' in src, 'button not in PLATFORMS'
223+
# Verify BACKUP_DIR in const
224+
with open('custom_components/luxtronik2_modbus_proxy/const.py') as f:
225+
src = f.read()
226+
assert 'BACKUP_DIR' in src, 'BACKUP_DIR not in const.py'
227+
print('Task 1 verification passed')
228+
"
229+
</automated>
230+
</verify>
231+
<done>
232+
- button.py exists with LuxtronikBackupButton class
233+
- Button has EntityCategory.CONFIG (appears under Configuration)
234+
- async_press reads all parameters via luxtronik library __new__ pattern
235+
- JSON saved to config/luxtronik2_backups/YYYY-MM-DD_HH-MM-SS.json
236+
- persistent_notification fires with parameter count, filename, timestamp
237+
- Backup metadata stored in hass.data for sensor consumption
238+
- "button" added to PLATFORMS in __init__.py
239+
- BACKUP_DIR constant added to const.py
240+
</done>
241+
</task>
242+
243+
<task type="auto">
244+
<name>Task 2: Add last backup sensor and update all translation files</name>
245+
<files>
246+
custom_components/luxtronik2_modbus_proxy/sensor.py,
247+
custom_components/luxtronik2_modbus_proxy/strings.json,
248+
custom_components/luxtronik2_modbus_proxy/translations/en.json,
249+
custom_components/luxtronik2_modbus_proxy/translations/de.json
250+
</files>
251+
<action>
252+
**sensor.py** — Add a "Last Backup" sensor entity.
253+
254+
This sensor does NOT use the coordinator data (it is not a Luxtronik register). Instead, it reads the backup metadata stored by button.py in `hass.data[DOMAIN][f"{entry_id}_last_backup"]`.
255+
256+
Add a new class `LuxtronikLastBackupSensor(SensorEntity)` (NOT CoordinatorEntity — it does not depend on poll data):
257+
- `_attr_name = "Luxtronik Last Backup"`
258+
- `_attr_icon = "mdi:backup-restore"`
259+
- `_attr_entity_category = EntityCategory.DIAGNOSTIC`
260+
- `_attr_unique_id = f"{entry.entry_id}_last_backup"`
261+
- `_attr_device_class = SensorDeviceClass.TIMESTAMP`
262+
- `device_info` property: same DeviceInfo pattern as other entities.
263+
- Store `hass`, `entry_id` references.
264+
265+
`native_value` property:
266+
- Read `hass.data[DOMAIN].get(f"{entry_id}_last_backup")`.
267+
- If None, return None (no backup yet).
268+
- Parse the ISO timestamp string and return as `datetime.datetime` object (required for TIMESTAMP device class).
269+
270+
`extra_state_attributes` property:
271+
- Return `{"filename": metadata["filename"]}` if backup metadata exists, else `{}`.
272+
273+
Wire the sensor to update when backup completes. In `async_added_to_hass()`:
274+
- Subscribe to dispatcher signal `f"{DOMAIN}_backup_complete"` using `async_dispatcher_connect`.
275+
- On signal, call `self.async_write_ha_state()` to refresh the sensor value.
276+
277+
In `async_will_remove_from_hass()`:
278+
- The dispatcher unsub is handled automatically if stored via `self.async_on_remove()`.
279+
280+
In `async_setup_entry()`: After the existing entity list creation, append one `LuxtronikLastBackupSensor(hass, entry)` to the entities list.
281+
282+
Add these imports to sensor.py:
283+
```python
284+
import datetime
285+
from homeassistant.helpers.dispatcher import async_dispatcher_connect
286+
```
287+
288+
**strings.json** — Add button and sensor entries to the `entity` section:
289+
```json
290+
"button": {
291+
"backup_parameters": {
292+
"name": "Backup Parameters"
293+
}
294+
},
295+
"sensor": {
296+
"last_backup": {
297+
"name": "Last Backup"
298+
}
299+
}
300+
```
301+
Note: The existing `entity` section does not have a `sensor` key yet (core sensors use hardcoded names). Add it alongside the existing `select` and `number` keys. Add `button` as a new key.
302+
303+
**translations/en.json** — Mirror the strings.json changes: add the same `button` and `sensor` keys inside the `entity` section.
304+
305+
**translations/de.json** — Add German translations:
306+
```json
307+
"button": {
308+
"backup_parameters": {
309+
"name": "Parameter-Sicherung"
310+
}
311+
},
312+
"sensor": {
313+
"last_backup": {
314+
"name": "Letzte Sicherung"
315+
}
316+
}
317+
```
318+
Add these inside the existing `entity` section alongside `select` and `number`.
319+
</action>
320+
<verify>
321+
<automated>cd /home/dwolbeck/claude-code/PUBLIC-luxtronik2-modbus-proxy && python -c "
322+
import ast, json, sys
323+
# Verify LuxtronikLastBackupSensor in sensor.py
324+
with open('custom_components/luxtronik2_modbus_proxy/sensor.py') as f:
325+
tree = ast.parse(f.read())
326+
classes = [n.name for n in ast.walk(tree) if isinstance(n, ast.ClassDef)]
327+
assert 'LuxtronikLastBackupSensor' in classes, f'Missing class, found: {classes}'
328+
# Verify strings.json has button entity
329+
with open('custom_components/luxtronik2_modbus_proxy/strings.json') as f:
330+
strings = json.load(f)
331+
assert 'button' in strings['entity'], 'button not in strings.json entity section'
332+
assert 'backup_parameters' in strings['entity']['button'], 'backup_parameters key missing'
333+
# Verify en.json
334+
with open('custom_components/luxtronik2_modbus_proxy/translations/en.json') as f:
335+
en = json.load(f)
336+
assert 'button' in en['entity'], 'button not in en.json'
337+
# Verify de.json
338+
with open('custom_components/luxtronik2_modbus_proxy/translations/de.json') as f:
339+
de = json.load(f)
340+
assert 'button' in de['entity'], 'button not in de.json'
341+
assert de['entity']['button']['backup_parameters']['name'] == 'Parameter-Sicherung'
342+
print('Task 2 verification passed')
343+
"
344+
</automated>
345+
</verify>
346+
<done>
347+
- LuxtronikLastBackupSensor class exists in sensor.py
348+
- Sensor has TIMESTAMP device class and shows last backup time
349+
- Sensor extra_state_attributes includes filename
350+
- Sensor updates via dispatcher signal when backup completes
351+
- strings.json has button.backup_parameters and sensor.last_backup entries
352+
- translations/en.json mirrors strings.json
353+
- translations/de.json has German translations (Parameter-Sicherung, Letzte Sicherung)
354+
</done>
355+
</task>
356+
357+
</tasks>
358+
359+
<verification>
360+
1. `python -c "import ast; ast.parse(open('custom_components/luxtronik2_modbus_proxy/button.py').read())"` — button.py is valid Python
361+
2. `python -c "import json; json.load(open('custom_components/luxtronik2_modbus_proxy/strings.json'))"` — strings.json is valid JSON
362+
3. `grep -c '"button"' custom_components/luxtronik2_modbus_proxy/__init__.py` — returns 1 (platform registered)
363+
4. All three translation files have matching button/sensor entity keys
364+
</verification>
365+
366+
<success_criteria>
367+
- A "Backup Parameters" button entity exists under the device's Configuration section
368+
- Pressing it reads all parameters via luxtronik library and saves JSON to /config/luxtronik2_backups/
369+
- JSON format matches spec: {timestamp, host, parameters: {index: {name, type, raw_value, display_value}}}
370+
- persistent_notification fires with backup summary
371+
- "Last Backup" sensor shows timestamp and filename attribute
372+
- All translation files (strings.json, en.json, de.json) updated
373+
- "button" registered in PLATFORMS list
374+
</success_criteria>
375+
376+
<output>
377+
After completion, create `.planning/quick/260409-m53-add-parameter-backup-button-and-backup-v/260409-m53-SUMMARY.md`
378+
</output>

0 commit comments

Comments
 (0)