Skip to content

Commit 35d6df3

Browse files
committed
fix(medicines): bundled list wins when newer; npl_id auto-fill
1 parent 2cf84d5 commit 35d6df3

5 files changed

Lines changed: 77 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [0.2.10] — 2026-05-10
4+
5+
Two fixes from v0.2.9 testing. Drop-in upgrade from 0.2.9.
6+
7+
- **Bundled medicines list wins when newer.** `MedicineDatabase.async_load` now compares `list_version` between the integration's bundled file and the user's stored copy; the lexicographically newer one wins (the `YYYY.MM.DD-N` format sorts correctly that way). Pre-v0.2.10, anyone who'd ever clicked **Refresh medicine list now** stayed pinned to that cached list across integration upgrades — the v0.2.9 jump from 216 to 7331 bundled entries was invisible until a manual re-refresh. Explicit URL refreshes ahead of the bundle still win.
8+
- **NPL ID auto-fills from the catalog.** Picking a known medicine in the Add/Edit modal now populates the NPL ID field the same way ATC code and active substance already do. Three spots fixed: `_normalize_entry` preserves `npl_id` on load (was being stripped), `sanitize_for_ws` forwards it to the panel over `pillpilot/get_medicines_db`, and `_applyDrugNameAutoFill` copies it into the draft when empty. User-entered NPL IDs are never overwritten.
9+
310
## [0.2.9] — 2026-05-10
411

512
Medicine list rebuilt from Läkemedelsverket. Drop-in upgrade from 0.2.8.

RELEASE_NOTES.md

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
1-
# v0.2.9
1+
# v0.2.10
22

3-
> Medicine list rebuilt from Läkemedelsverket. Drop-in upgrade from 0.2.8.
3+
> Two fixes from v0.2.9 testing. Drop-in upgrade from 0.2.9.
44
5-
## What's new
5+
## What's fixed
66

7-
`medicines_se.json` is now generated from [Läkemedelsverket's open-data register](https://www.dataportal.se/datasets/140_5467) (Sök läkemedelsfakta, dataset 140_5467) instead of being hand-curated. 216 entries → 7331. Covers every human medicine currently `Godkänd` or `Registrerad`. Veterinary and deregistered products are filtered at build time. Snapshot 2026-05-10.
7+
**Bundled medicines list wins when newer.** Pre-v0.2.10 the stored copy in `.storage/pillpilot.medicines_se` always won over the integration's bundled file. So if you'd ever clicked **Refresh medicine list now** in an earlier release, you stayed stuck on that cached list across HACS upgrades — the v0.2.9 jump from 216 to 7331 entries was invisible until you manually refreshed again. `MedicineDatabase.async_load` now compares `list_version` on the two and picks the newer one. Explicit URL refreshes ahead of the bundle still take precedence.
88

9-
Each entry now carries `npl_id`, and `aliases` includes former product names from the `Tidigare läkemedelsnamn` column — searching an old brand name finds the current entry.
10-
11-
## Build tool
12-
13-
`tools/build_medicines_se.py` regenerates the JSON from a fresh `Lakemedelsprodukter.xlsx` export. Maintainer tool — not shipped in the integration zip.
14-
15-
```
16-
pip install openpyxl
17-
python tools/build_medicines_se.py \
18-
--input ~/Downloads/Lakemedelsprodukter.xlsx \
19-
--output custom_components/pillpilot/medicines_se.json
20-
```
21-
22-
Curated aliases on existing entries are preserved across rebuilds.
9+
**NPL ID auto-fills from the catalog.** When you pick a known medicine in the panel's Add/Edit modal, the NPL ID field now populates from the Läkemedelsverket export, the same way ATC code and active substance already do. Three spots that all had to be in sync: `_normalize_entry` preserves the field on load, `sanitize_for_ws` forwards it to the panel, and `_applyDrugNameAutoFill` writes it into the draft. Anything you typed yourself isn't overwritten.
2310

2411
## Upgrading
2512

26-
Replace the `pillpilot` directory in `custom_components/` with the contents of this zip and restart Home Assistant. HACS users: update normally. Existing medicines are unaffected.
13+
Replace the `pillpilot` directory in `custom_components/` with the contents of this zip and restart Home Assistant. HACS users: update normally.
2714

28-
ATC codes come straight from Läkemedelsverket. Still verify against FASS or Sök VARA before relying on them clinically.
15+
Anyone on the v0.2.9 bundled list (`2026.05.10-1`) and a stored copy from before will be flipped to the bundled list on first load after this upgrade. Existing medicines configured before the upgrade are unaffected.

custom_components/pillpilot/frontend/panel.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,8 +1565,8 @@ class PillPilotPanel extends HTMLElement {
15651565
}
15661566

15671567
// Apply auto-fill to the Add/Edit draft based on a typed-or-picked
1568-
// value in the drug-name field. Fills empty atc_code + notes from
1569-
// the matched catalog entry; never overwrites user-entered values.
1568+
// value in the drug-name field. Fills empty atc_code + npl_id + notes
1569+
// from the matched catalog entry; never overwrites user-entered values.
15701570
// Returns true if anything in the draft changed (caller decides
15711571
// whether to re-render).
15721572
_applyDrugNameAutoFill(typedValue) {
@@ -1588,6 +1588,11 @@ class PillPilotPanel extends HTMLElement {
15881588
draft.drug.atc_code = hit.atc_code;
15891589
changed = true;
15901590
}
1591+
const userNpl = (draft.drug.npl_id || "").trim();
1592+
if (!userNpl && hit.npl_id) {
1593+
draft.drug.npl_id = hit.npl_id;
1594+
changed = true;
1595+
}
15911596
const userNotes = (draft.drug.notes || "").trim();
15921597
if (!userNotes && hit.active_substance) {
15931598
draft.drug.notes = `Aktiv substans: ${hit.active_substance}`;

custom_components/pillpilot/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"domain": "pillpilot",
33
"name": "PillPilot",
4-
"version": "0.2.9",
4+
"version": "0.2.10",
55
"documentation": "https://github.com/TSA3000/ha-pillpilot",
66
"issue_tracker": "https://github.com/TSA3000/ha-pillpilot/issues",
77
"codeowners": ["@TSA3000"],

custom_components/pillpilot/medicines.py

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def _normalize_entry(raw: dict[str, Any]) -> dict[str, Any] | None:
8484
"aliases": [str(a).strip() for a in aliases_raw if str(a).strip()],
8585
"active_substance": str(raw.get("active_substance") or "").strip(),
8686
"atc_code": str(raw.get("atc_code") or "").strip(),
87+
"npl_id": str(raw.get("npl_id") or "").strip(),
8788
"common_forms": [str(f).strip() for f in forms_raw if str(f).strip()],
8889
}
8990

@@ -144,35 +145,65 @@ def is_loaded(self) -> bool:
144145
return self._loaded
145146

146147
async def async_load(self) -> None:
147-
"""Load on integration startup. Stored copy wins over bundled."""
148+
"""Load on integration startup. Bundled wins if it's newer.
149+
150+
v0.2.10: compares ``list_version`` between the integration's
151+
bundled file and any stored copy. The lexicographically newer
152+
one wins — the ``YYYY.MM.DD-N`` format sorts correctly that
153+
way. Pre-v0.2.10 the stored copy always won, so users who'd
154+
ever clicked **Refresh** stayed pinned to that cached list
155+
across integration upgrades and never saw new bundled
156+
medicines (e.g. the v0.2.9 jump from 216 → 7331 entries was
157+
invisible to anyone with a stored copy). Explicit URL
158+
refreshes still win when the URL is ahead of the bundle.
159+
"""
148160
stored = await self._store.async_load()
161+
stored_version = ""
149162
if stored and isinstance(stored, dict) and stored.get("medicines"):
150-
self._data = _normalize_list(stored)
151-
self._loaded = True
152-
_LOGGER.debug(
153-
"Loaded medicines list from storage (version=%s, count=%d)",
154-
self._data["list_version"], len(self._data["medicines"]),
155-
)
156-
return
157-
# Fall back to bundled file
163+
stored_version = str(stored.get("list_version") or "")
164+
165+
bundled_data: dict[str, Any] | None = None
158166
try:
159167
bundled_text = await self._hass.async_add_executor_job(
160168
BUNDLED_PATH.read_text, "utf-8"
161169
)
162-
self._data = _normalize_list(json.loads(bundled_text))
163-
self._loaded = True
164-
_LOGGER.debug(
165-
"Loaded bundled medicines list (version=%s, count=%d)",
166-
self._data["list_version"], len(self._data["medicines"]),
167-
)
170+
bundled_data = json.loads(bundled_text)
168171
except (OSError, json.JSONDecodeError) as err:
169-
_LOGGER.error(
172+
_LOGGER.warning(
170173
"Could not load bundled medicines list at %s: %s",
171174
BUNDLED_PATH, err,
172175
)
173-
# Empty doc — autocomplete dropdown will simply have no options
174-
self._data = _empty_doc()
176+
bundled_version = str((bundled_data or {}).get("list_version") or "")
177+
178+
if bundled_data and (
179+
not stored_version or bundled_version > stored_version
180+
):
181+
self._data = _normalize_list(bundled_data)
175182
self._loaded = True
183+
_LOGGER.debug(
184+
"Loaded bundled medicines list (version=%s, count=%d, "
185+
"stored=%s)",
186+
self._data["list_version"], len(self._data["medicines"]),
187+
stored_version or "none",
188+
)
189+
return
190+
191+
if stored and isinstance(stored, dict) and stored.get("medicines"):
192+
self._data = _normalize_list(stored)
193+
self._loaded = True
194+
_LOGGER.debug(
195+
"Loaded medicines list from storage (version=%s, count=%d, "
196+
"bundled=%s)",
197+
self._data["list_version"], len(self._data["medicines"]),
198+
bundled_version or "none",
199+
)
200+
return
201+
202+
_LOGGER.error(
203+
"Could not load any medicines list (stored=missing, bundled=missing)"
204+
)
205+
self._data = _empty_doc()
206+
self._loaded = True
176207

177208
async def async_refresh_from_url(self, url: str) -> tuple[bool, str]:
178209
"""Fetch a fresh medicines list from `url` and persist it.
@@ -323,11 +354,11 @@ def sanitize_for_ws(
323354
) -> list[dict[str, Any]]:
324355
"""Project the medicines list down to the fields the panel needs.
325356
326-
The panel's autocomplete/auto-fill needs only ``name``, ``aliases``,
327-
``active_substance`` and ``atc_code``. The bundled list also carries
328-
``common_forms``, vendor metadata, and pre-list comments — all
329-
irrelevant on the wire and worth not shipping over the websocket
330-
on every panel load.
357+
The panel's autocomplete/auto-fill needs ``name``, ``aliases``,
358+
``active_substance``, ``atc_code`` and (since v0.2.10) ``npl_id``.
359+
The bundled list also carries ``common_forms``, vendor metadata,
360+
and pre-list comments — all irrelevant on the wire and worth not
361+
shipping over the websocket on every panel load.
331362
332363
Nameless entries are dropped; missing optional fields are emitted
333364
as empty strings so the panel can rely on the shape.
@@ -347,5 +378,6 @@ def sanitize_for_ws(
347378
"aliases": aliases,
348379
"active_substance": (med.get("active_substance") or "").strip(),
349380
"atc_code": (med.get("atc_code") or "").strip(),
381+
"npl_id": (med.get("npl_id") or "").strip(),
350382
})
351383
return out

0 commit comments

Comments
 (0)