Skip to content

Commit 2ec2ddb

Browse files
gbroecklingclaude
andcommitted
fix: map scale save, occupancy training save, blocking scandir, onboarding click
- maps.js: pre-compute scale_x_m/scale_y_m for button label (was ReferenceError) - websocket.py: use _st.store.async_save() not _st.async_save() (AttributeError) - websocket.py: wrap iterdir/is_dir in asyncio.to_thread (blocking call warning) - panel.js: guard this.actions?.renderRooms with fallback (TypeError) Closes user report: map changes not saving, 25 frontend errors in Edge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7ff84c9 commit 2ec2ddb

6 files changed

Lines changed: 86 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,80 @@ All notable changes to PadSpan HA are documented here.
44

55
---
66

7+
## 0.17.2 — Bug Fix (2026-03-24)
8+
9+
### Fixed
10+
- **Map scale save crash** — "Save Scale" button referenced `scale_x_m` / `scale_y_m` before they were defined, causing `ReferenceError` and preventing scale saves (maps.js:1944)
11+
- **Occupancy training save crash**`ws_occupancy_train` called `_st.async_save()` which doesn't exist on `SettingsStore`; corrected to `_st.store.async_save(_st.data)` (websocket.py:8374)
12+
- **Blocking `scandir` in event loop** — factory reset's map-file cleanup used synchronous `iterdir()` / `is_dir()` inside an async handler, triggering HA's blocking-call detector; wrapped in `asyncio.to_thread` (websocket.py:7508)
13+
- **Onboarding step click crash**`this.actions.renderRooms()` could fail with `TypeError` if `actions` was undefined during panel init; added optional chaining with fallback (panel.js:2414)
14+
15+
---
16+
17+
## 0.17.0 — Stable Release (2026-03-23)
18+
19+
Major release with 78 commits since last stable (v0.15.25). Introduces the Device Registry identity system, positioning fabric decoupling, multi-floor accuracy learning, occupancy estimation, and an onboarding wizard.
20+
21+
### Device Registry (NEW)
22+
- **Stable device identity** — every physical device gets an immutable `padspan_id` (format: `ps_<12 hex chars>`) that survives MAC rotation, iBeacon UUID changes, and firmware updates
23+
- **Identity resolution** — O(1) lookup from any volatile key (MAC, iBeacon, canonical_id) to stable padspan_id
24+
- **Automatic migration** — existing labeled objects in ObjectStore are auto-migrated to DeviceRegistry on first startup
25+
- **Label pipeline** — DeviceRegistry is now the primary label source; ObjectStore is a thin fallback
26+
- **HA entity identity** — sensor and device_tracker entities use padspan_id for stable HA device identity
27+
- **Frontend management** — Devices view has interactive registry: merge duplicates, add identities, relabel, delete, view identity chains
28+
- **7 WS commands** — list, migrate, merge, resolve, label_set, add_identity, delete
29+
- **Health checks** — Device Registry status, Label Pipeline health, dependent store migration progress
30+
31+
### Positioning Fabric (decoupling from maps)
32+
- **Fabric is the authority** — all spatial data (scanner positions, room geometry, RF barriers, beacon positions) stored in real-world metres in the positioning fabric
33+
- **Maps are setup tools only** — floor plan images no longer own positioning data, overview map toggle defaults to off
34+
- **Metre-space coordinates** — all stores use real-world metres with floor_id references
35+
- **Map transforms** — affine transforms convert between map fracs and metres
36+
- **Measure tool** — two-point reference distance calibration with aspect ratio validation
37+
38+
### Multi-Floor Accuracy
39+
- **Floor-transition learning** — adaptive store records floor-to-floor transitions with Welford stats on dwell time
40+
- **Dwell-based velocity gate** — short dwell (<30s) requires unanimous vote; medium dwell (30-120s) needs supermajority for cross-floor; long dwell (>120s) uses normal threshold
41+
- **Learned cross-floor attenuation** — Gaussian scorer applies learned RSSI corrections to cross-floor scanners when adaptive floor detection is enabled
42+
- **Outdoor penalties** — outdoor scanners get 0.30x Gaussian damping; indoor-outdoor transitions require 4x floor stickiness
43+
44+
### Occupancy Estimation
45+
- **Dedicated Occupancy dashboard** — new sidebar view with building summary, per-room breakdown, training controls, and training history
46+
- **Hybrid counting** — identified devices count 1:1, unidentified BLE with dwell >5min count with configurable multiplier (default 1.5x)
47+
- **Training** — enter actual headcount to adjust the multiplier via EMA learning
48+
49+
### Onboarding Wizard
50+
- **Guided first-run setup** — persistent progress bar detects 5 steps: upload floor plan, set scale, place scanners, draw rooms, calibrate
51+
- **Auto-detection** — each step auto-completes when its data is detected
52+
- **Click-to-navigate** — each step links directly to the right view
53+
- **Skip option** — dismisses permanently via settings
54+
55+
### Calibration & Beacon Tune Fixes
56+
- **Room polygons no longer block dragging**`pointer-events: none` on room polygons in both Tune and Beacon Tune
57+
- **Save-pulse animation** — save button pulses green when there are unsaved changes (dynamically updated after drags)
58+
- **Beacon sync to maps** — fabric beacons are now synced back to maps store for consistent rendering
59+
- **Unique beacon IDs** — prevents drag handler from matching wrong beacon when multiple have empty IDs
60+
- **SVG not rebuilt mid-drag**`_refreshSVG()` checks `_dragging` flag
61+
- **Watchdog fix** — no longer force-renders on non-live views (was disrupting calibration mid-drag)
62+
- **Out-of-bounds beacons filtered** — beacons outside map coordinate range are skipped instead of clamped to edges
63+
64+
### Distance Traveled
65+
- **Fixed data reading** — was reading `frame.objects` instead of `frame.o` (compact format), producing zero distance for everything
66+
- **Jitter filtering** — steps <0.5m ignored, same-room capped at 3m, time-gap scaling for downsampled views
67+
- **Reliability score** — shows what % of position steps passed the jitter filter
68+
- **Investigate button** — popup showing total steps, good steps, jitter filtered, max step
69+
- **Stationary references** — mark known-fixed devices as references; their phantom distance becomes a BLE accuracy diagnostic
70+
- **BLE Accuracy rating** — Excellent/Good/Fair/Poor based on total phantom distance from reference devices
71+
72+
### Other
73+
- **Donate button** added to README (PayPal)
74+
- **Traceback** — padspan_id recorded on each frame object for stable history
75+
- **Movement history** — padspan_id on room transition records
76+
- **Follow alerts** — padspan_id auto-backfilled on startup
77+
- **`padspan_id` in HA entity attributes** — visible in developer tools on area sensors and device trackers
78+
79+
---
80+
781
## 0.5.91 — Hardware Guide & Cleanup (2026-03-01)
882

983
### Added

VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
padspanHA package version: 0.17.1
1+
padspanHA package version: 0.17.2

custom_components/padspan_ha/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@
2020
"issue_tracker": "https://github.com/gbroeckling/padspanHA/issues",
2121
"requirements": [],
2222
"single_config_entry": true,
23-
"version": "0.17.1"
23+
"version": "0.17.2"
2424
}

custom_components/padspan_ha/websocket.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7504,8 +7504,8 @@ def _adaptive_empty():
75047504
_mdir = maps_obj.maps_dir
75057505
else:
75067506
_mdir = _Path(hass.config.path("www")) / "padspan_ha" / "maps"
7507-
if _mdir.is_dir():
7508-
for f in _mdir.iterdir():
7507+
if await _aio.to_thread(_mdir.is_dir):
7508+
for f in await _aio.to_thread(list, _mdir.iterdir()):
75097509
if f.is_file() and f.suffix.lower() in (".png", ".jpg", ".jpeg", ".webp"):
75107510
try:
75117511
await _aio.to_thread(f.unlink)
@@ -8371,7 +8371,7 @@ async def ws_occupancy_train(hass: HomeAssistant, connection, msg) -> None:
83718371
if len(training) > 100:
83728372
training = training[-100:]
83738373
_st.data["occupancy_training"] = training
8374-
await _st.async_save()
8374+
await _st.store.async_save(_st.data)
83758375

83768376
connection.send_result(msg["id"], {"ok": True, "observation": observation, "total_observations": len(training)})
83778377

custom_components/padspan_ha/www/padspan-ha/panel.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2411,7 +2411,8 @@ class PadSpanHaApp extends HTMLElement {
24112411
if (isNext) row.appendChild(el("span",{style:"font-size:10px;color:#94a3b8;margin-left:auto"}, s.hint));
24122412
row.addEventListener("click", () => {
24132413
this.state.view = s.view;
2414-
this.actions.renderRooms();
2414+
if (this.actions?.renderRooms) this.actions.renderRooms();
2415+
else this._renderCurrentView();
24152416
});
24162417
list.appendChild(row);
24172418
}

custom_components/padspan_ha/www/padspan-ha/views/maps.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1939,9 +1939,13 @@ function _edit(ctx, map){
19391939
}
19401940
mPanel.appendChild(analysisDiv);
19411941

1942+
// Pre-compute dimensions for the button label (same formula as inside the click handler)
1943+
const _preview_x_m = Math.round((imgW / avgPpm) * 10000) / 10000;
1944+
const _preview_y_m = Math.round((imgH / avgPpm) * 10000) / 10000;
1945+
19421946
// Save button — prominent green to make it clear this is the save action
19431947
const applyBtn = el("button",{class:"btn save-pulse",style:"margin-top:12px;width:100%;padding:12px;font-size:15px;background:#1a3a0a;border:2px solid #52b788;color:#86efac;font-weight:800;border-radius:8px;cursor:pointer"},
1944-
`\ud83d\udcbe Save Scale: ${avgPpm.toFixed(1)} px/m \u2192 ${scale_x_m.toFixed(1)}m \u00d7 ${scale_y_m.toFixed(1)}m`);
1948+
`\ud83d\udcbe Save Scale: ${avgPpm.toFixed(1)} px/m \u2192 ${_preview_x_m.toFixed(1)}m \u00d7 ${_preview_y_m.toFixed(1)}m`);
19451949
applyBtn.addEventListener("click", async () => {
19461950
applyBtn.disabled = true; applyBtn.textContent = "Saving\u2026";
19471951
const ppm = Math.round(avgPpm * 100) / 100;

0 commit comments

Comments
 (0)