From 5e043d45f2efeb77c034dce3345872d3afde950f Mon Sep 17 00:00:00 2001 From: thekiwismarthome <134335563+thekiwismarthome@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:40:30 +1300 Subject: [PATCH] v1.5.0 - Added dropdown list for Lists --- .../shopping_list_manager/__init__.py | 6 +- .../shopping_list_manager/const.py | 1 + .../shopping_list_manager/manager.py | 86 ++++++++++++++++++- .../shopping_list_manager/manifest.json | 2 +- .../shopping_list_manager/websocket_api.py | 42 +++++++++ 5 files changed, 131 insertions(+), 6 deletions(-) diff --git a/custom_components/shopping_list_manager/__init__.py b/custom_components/shopping_list_manager/__init__.py index 5617f08..e092b15 100644 --- a/custom_components/shopping_list_manager/__init__.py +++ b/custom_components/shopping_list_manager/__init__.py @@ -16,6 +16,8 @@ from .websocket_api import ( websocket_get_products, websocket_get_active, websocket_delete_product, + ws_get_catalogues, + ws_get_lists, ) _LOGGER = logging.getLogger(__name__) @@ -37,8 +39,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ha_websocket.async_register_command(hass, websocket_get_products) ha_websocket.async_register_command(hass, websocket_get_active) ha_websocket.async_register_command(hass, websocket_delete_product) + ha_websocket.async_register_command(hass, ws_get_catalogues) + ha_websocket.async_register_command(hass, ws_get_lists) - _LOGGER.info("Shopping List Manager setup complete - registered 5 WebSocket commands") + _LOGGER.info("Shopping List Manager setup complete - registered 7 WebSocket commands") return True diff --git a/custom_components/shopping_list_manager/const.py b/custom_components/shopping_list_manager/const.py index 36c7872..bdb838e 100644 --- a/custom_components/shopping_list_manager/const.py +++ b/custom_components/shopping_list_manager/const.py @@ -6,6 +6,7 @@ DOMAIN = "shopping_list_manager" STORAGE_VERSION = 1 STORAGE_KEY_PRODUCTS = f"{DOMAIN}.products" STORAGE_KEY_ACTIVE = f"{DOMAIN}.active_list" +LISTS_STORE_KEY = "shopping_list_manager.lists" # Events EVENT_SHOPPING_LIST_UPDATED = f"{DOMAIN}_updated" diff --git a/custom_components/shopping_list_manager/manager.py b/custom_components/shopping_list_manager/manager.py index 7f228ef..cc7bf77 100644 --- a/custom_components/shopping_list_manager/manager.py +++ b/custom_components/shopping_list_manager/manager.py @@ -5,6 +5,8 @@ from typing import Dict, Optional, List from homeassistant.core import HomeAssistant from homeassistant.helpers import storage +from homeassistant.helpers.storage import Store + from .const import ( DOMAIN, @@ -58,7 +60,21 @@ class ShoppingListManager: self._store_active["groceries"] = storage.Store( hass, STORAGE_VERSION, STORAGE_KEY_ACTIVE # "shopping_list_manager.active_list" ) - + # --- Catalogue + list metadata (NEW, additive) --- + self._store_catalogues = Store( + hass, + STORAGE_VERSION, + f"{DOMAIN}.catalogues", + ) + self._catalogues: Dict[str, dict] = {} + + self._store_lists = Store( + hass, + STORAGE_VERSION, + f"{DOMAIN}.lists", + ) + self._lists: Dict[str, dict] = {} + def _lock_for(self, list_id: str) -> asyncio.Lock: """Get or create lock for a list.""" if list_id not in self._locks: @@ -68,10 +84,20 @@ class ShoppingListManager: def _store_products_for(self, list_id: str) -> storage.Store: """Get or create products Store for a list.""" if list_id not in self._store_products: - # Non-groceries lists use namespaced keys - key = f"{DOMAIN}.{list_id}.products" - self._store_products[list_id] = storage.Store(self.hass, STORAGE_VERSION, key) + catalogue_id = self._lists.get(list_id, {}).get("catalogue", list_id) + catalogue = self._catalogues.get(catalogue_id) + + key = ( + catalogue["products_store"] + if catalogue + else f"{DOMAIN}.{list_id}.products" + ) + + self._store_products[list_id] = storage.Store( + self.hass, STORAGE_VERSION, key + ) return self._store_products[list_id] + def _store_active_for(self, list_id: str) -> storage.Store: """Get or create active Store for a list.""" @@ -82,6 +108,17 @@ class ShoppingListManager: async def _ensure_loaded(self, list_id: str) -> None: """Lazily load a list from storage if not yet in memory.""" + await self._ensure_catalogues_loaded() + await self._ensure_lists_loaded() + + # Register list if it does not exist yet + if list_id not in self._lists: + self._lists[list_id] = { + "catalogue": list_id + } + await self._store_lists.async_save(self._lists) + + if list_id in self._products: return # already loaded @@ -103,6 +140,38 @@ class ShoppingListManager: list_id, len(self._products[list_id]), len(self._active_list[list_id]) ) + async def _ensure_catalogues_loaded(self) -> None: + data = await self._store_catalogues.async_load() + if isinstance(data, dict): + self._catalogues = data + return + + # Bootstrap from existing behavior (no changes) + self._catalogues = { + "groceries": { + "name": "Groceries", + "icon": "🛒", + "products_store": f"{DOMAIN}.products", + } + } + + await self._store_catalogues.async_save(self._catalogues) + + async def _ensure_lists_loaded(self) -> None: + data = await self._store_lists.async_load() + if isinstance(data, dict): + self._lists = data + return + + # Default: list_id == catalogue_id (current behavior) + self._lists = { + "groceries": { + "catalogue": "groceries", + } + } + + await self._store_lists.async_save(self._lists) + async def async_load(self) -> None: """Pre-load the default groceries list for backward compat.""" async with self._lock_for("groceries"): @@ -265,6 +334,15 @@ class ShoppingListManager: _LOGGER.debug("List '%s': deleted product: %s", list_id, key) self._fire_update_event() + def get_catalogues(self) -> Dict[str, dict]: + return self._catalogues + + async def async_get_lists(self) -> Dict[str, dict]: + await self._ensure_catalogues_loaded() + await self._ensure_lists_loaded() + return self._lists + + async def async_get_products(self, list_id: str) -> Dict[str, dict]: """ Get all products in a list's catalog. diff --git a/custom_components/shopping_list_manager/manifest.json b/custom_components/shopping_list_manager/manifest.json index 210ec8a..0e2d5c9 100644 --- a/custom_components/shopping_list_manager/manifest.json +++ b/custom_components/shopping_list_manager/manifest.json @@ -1,7 +1,7 @@ { "domain": "shopping_list_manager", "name": "Shopping List Manager", - "version": "1.0.0", + "version": "1.5.0", "documentation": "https://github.com/thekiwismarthome/shopping-list-manager", "issue_tracker": "https://github.com/thekiwismarthome/shopping-list-manager/issues", "requirements": [], diff --git a/custom_components/shopping_list_manager/websocket_api.py b/custom_components/shopping_list_manager/websocket_api.py index f248d32..f7d61dc 100644 --- a/custom_components/shopping_list_manager/websocket_api.py +++ b/custom_components/shopping_list_manager/websocket_api.py @@ -74,6 +74,48 @@ async def websocket_add_product( connection.send_error(msg["id"], "add_product_failed", str(err)) +@websocket_api.websocket_command({ + vol.Required("type"): "shopping_list_manager/get_catalogues", +}) +@websocket_api.async_response +async def ws_get_catalogues( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Return catalogue metadata (read-only).""" + manager = hass.data[DOMAIN]["manager"] + + try: + catalogues = manager.get_catalogues() + connection.send_result(msg["id"], catalogues) + except Exception as err: + _LOGGER.error("Error getting catalogues: %s", err) + connection.send_error(msg["id"], "get_catalogues_failed", str(err)) + + +@websocket_api.websocket_command({ + vol.Required("type"): "shopping_list_manager/get_lists", +}) +@websocket_api.async_response +async def ws_get_lists( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Return list → catalogue mapping (read-only).""" + manager = hass.data[DOMAIN]["manager"] + + try: + # Ensure lists are loaded + await manager._ensure_lists_loaded() + lists = manager._lists + connection.send_result(msg["id"], lists) + except Exception as err: + _LOGGER.error("Error getting lists: %s", err) + connection.send_error(msg["id"], "get_lists_failed", str(err)) + + @websocket_api.websocket_command({ vol.Required("type"): "shopping_list_manager/set_qty", vol.Optional("list_id", default="groceries"): str,