From c114645f881d3aa66d86521a9ee31f42b6da626d Mon Sep 17 00:00:00 2001 From: thekiwismarthome <134335563+thekiwismarthome@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:37:55 +1300 Subject: [PATCH 1/5] Update manager.py --- .../shopping_list_manager/manager.py | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/custom_components/shopping_list_manager/manager.py b/custom_components/shopping_list_manager/manager.py index cc7bf77..914bcf8 100644 --- a/custom_components/shopping_list_manager/manager.py +++ b/custom_components/shopping_list_manager/manager.py @@ -160,13 +160,34 @@ class ShoppingListManager: async def _ensure_lists_loaded(self) -> None: data = await self._store_lists.async_load() if isinstance(data, dict): + import time + + # Migration: ensure new metadata fields exist + for list_id, meta in data.items(): + if "owner" not in meta: + meta["owner"] = "system" + if "visibility" not in meta: + meta["visibility"] = "shared" + if "created_at" not in meta: + meta["created_at"] = time.time() + if "updated_at" not in meta: + meta["updated_at"] = time.time() + self._lists = data + await self._store_lists.async_save(self._lists) return + # Default: list_id == catalogue_id (current behavior) + import time + self._lists = { "groceries": { "catalogue": "groceries", + "owner": "system", + "visibility": "shared", + "created_at": time.time(), + "updated_at": time.time(), } } @@ -336,6 +357,17 @@ class ShoppingListManager: def get_catalogues(self) -> Dict[str, dict]: return self._catalogues + + def get_visible_lists(self, user): + if user.is_admin: + return self._lists + + return { + lid: meta + for lid, meta in self._lists.items() + if meta.get("visibility") == "shared" + or meta.get("owner") == user.id + } async def async_get_lists(self) -> Dict[str, dict]: await self._ensure_catalogues_loaded() @@ -375,4 +407,4 @@ class ShoppingListManager: # and would need updating to support per-list structure: # - async_get_full_state() # - get_product() - # - get_active_qty() \ No newline at end of file + # - get_active_qty() From 78829aefb048e84e6c76c7265783d75699911854 Mon Sep 17 00:00:00 2001 From: thekiwismarthome <134335563+thekiwismarthome@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:38:28 +1300 Subject: [PATCH 2/5] Update websocket_api.py --- .../shopping_list_manager/websocket_api.py | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/custom_components/shopping_list_manager/websocket_api.py b/custom_components/shopping_list_manager/websocket_api.py index f7d61dc..42031e1 100644 --- a/custom_components/shopping_list_manager/websocket_api.py +++ b/custom_components/shopping_list_manager/websocket_api.py @@ -56,7 +56,15 @@ async def websocket_add_product( """ manager = hass.data[DOMAIN]["manager"] list_id = msg.get("list_id", "groceries") - + lists = manager.get_visible_lists(connection.user) + if list_id not in lists: + connection.send_error( + msg["id"], + "not_authorized", + f"You do not have access to list '{list_id}'" + ) + return + try: product = await manager.async_add_product( list_id=list_id, @@ -109,7 +117,7 @@ async def ws_get_lists( try: # Ensure lists are loaded await manager._ensure_lists_loaded() - lists = manager._lists + lists = manager.get_visible_lists(connection.user) connection.send_result(msg["id"], lists) except Exception as err: _LOGGER.error("Error getting lists: %s", err) @@ -159,6 +167,14 @@ async def websocket_set_qty( """ manager = hass.data[DOMAIN]["manager"] list_id = msg.get("list_id", "groceries") + lists = manager.get_visible_lists(connection.user) + if list_id not in lists: + connection.send_error( + msg["id"], + "not_authorized", + f"You do not have access to list '{list_id}'" + ) + return try: await manager.async_set_qty( @@ -212,6 +228,14 @@ async def websocket_get_products( """ manager = hass.data[DOMAIN]["manager"] list_id = msg.get("list_id", "groceries") + lists = manager.get_visible_lists(connection.user) + if list_id not in lists: + connection.send_error( + msg["id"], + "not_authorized", + f"You do not have access to list '{list_id}'" + ) + return try: products = await manager.async_get_products(list_id=list_id) @@ -250,6 +274,14 @@ async def websocket_get_active( """ manager = hass.data[DOMAIN]["manager"] list_id = msg.get("list_id", "groceries") + lists = manager.get_visible_lists(connection.user) + if list_id not in lists: + connection.send_error( + msg["id"], + "not_authorized", + f"You do not have access to list '{list_id}'" + ) + return try: active = await manager.async_get_active(list_id=list_id) @@ -288,6 +320,14 @@ async def websocket_delete_product( """ manager = hass.data[DOMAIN]["manager"] list_id = msg.get("list_id", "groceries") + lists = manager.get_visible_lists(connection.user) + if list_id not in lists: + connection.send_error( + msg["id"], + "not_authorized", + f"You do not have access to list '{list_id}'" + ) + return try: await manager.async_delete_product(list_id=list_id, key=msg["key"]) @@ -295,4 +335,4 @@ async def websocket_delete_product( except Exception as err: _LOGGER.error("Error deleting product from list '%s': %s", list_id, err) - connection.send_error(msg["id"], "delete_product_failed", str(err)) \ No newline at end of file + connection.send_error(msg["id"], "delete_product_failed", str(err)) From b6941a14991d811e8309b9391a7d134dec25f371 Mon Sep 17 00:00:00 2001 From: thekiwismarthome <134335563+thekiwismarthome@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:02:22 +1300 Subject: [PATCH 3/5] Update __init__.py --- custom_components/shopping_list_manager/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/shopping_list_manager/__init__.py b/custom_components/shopping_list_manager/__init__.py index e092b15..56cbb4d 100644 --- a/custom_components/shopping_list_manager/__init__.py +++ b/custom_components/shopping_list_manager/__init__.py @@ -6,6 +6,8 @@ import logging from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.components import websocket_api as ha_websocket +from .websocket_api import websocket_create_list + from .const import DOMAIN from .manager import ShoppingListManager @@ -34,6 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN]["manager"] = manager # Register WebSocket commands using Home Assistant's websocket_api + ha_websocket.async_register_command(hass, websocket_create_list) ha_websocket.async_register_command(hass, websocket_add_product) ha_websocket.async_register_command(hass, websocket_set_qty) ha_websocket.async_register_command(hass, websocket_get_products) @@ -50,4 +53,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Shopping List Manager.""" hass.data[DOMAIN].pop("manager", None) - return True \ No newline at end of file + return True From 0eb06f240da510d25aba73cde4f9653f3eed3c13 Mon Sep 17 00:00:00 2001 From: thekiwismarthome <134335563+thekiwismarthome@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:02:49 +1300 Subject: [PATCH 4/5] Update websocket_api.py --- .../shopping_list_manager/websocket_api.py | 71 ++++++++----------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/custom_components/shopping_list_manager/websocket_api.py b/custom_components/shopping_list_manager/websocket_api.py index 42031e1..da53906 100644 --- a/custom_components/shopping_list_manager/websocket_api.py +++ b/custom_components/shopping_list_manager/websocket_api.py @@ -10,6 +10,33 @@ from .models import InvariantError _LOGGER = logging.getLogger(__name__) +@websocket_api.websocket_command({ + vol.Required("type"): "shopping_list_manager/create_list", + vol.Required("list_id"): str, + vol.Required("catalogue"): str, + vol.Optional("visibility", default="shared"): vol.In(["shared", "private"]), +}) +@websocket_api.async_response +async def websocket_create_list( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + manager = hass.data[DOMAIN]["manager"] + + try: + await manager.async_create_list( + list_id=msg["list_id"], + catalogue=msg["catalogue"], + owner=connection.user.id, + visibility=msg.get("visibility", "shared"), + ) + + connection.send_result(msg["id"], {"success": True}) + + except Exception as err: + connection.send_error(msg["id"], "create_list_failed", str(err)) + @websocket_api.websocket_command({ vol.Required("type"): "shopping_list_manager/add_product", @@ -56,15 +83,7 @@ async def websocket_add_product( """ manager = hass.data[DOMAIN]["manager"] list_id = msg.get("list_id", "groceries") - lists = manager.get_visible_lists(connection.user) - if list_id not in lists: - connection.send_error( - msg["id"], - "not_authorized", - f"You do not have access to list '{list_id}'" - ) - return - + try: product = await manager.async_add_product( list_id=list_id, @@ -117,7 +136,7 @@ async def ws_get_lists( try: # Ensure lists are loaded await manager._ensure_lists_loaded() - lists = manager.get_visible_lists(connection.user) + lists = manager._lists connection.send_result(msg["id"], lists) except Exception as err: _LOGGER.error("Error getting lists: %s", err) @@ -167,14 +186,6 @@ async def websocket_set_qty( """ manager = hass.data[DOMAIN]["manager"] list_id = msg.get("list_id", "groceries") - lists = manager.get_visible_lists(connection.user) - if list_id not in lists: - connection.send_error( - msg["id"], - "not_authorized", - f"You do not have access to list '{list_id}'" - ) - return try: await manager.async_set_qty( @@ -228,14 +239,6 @@ async def websocket_get_products( """ manager = hass.data[DOMAIN]["manager"] list_id = msg.get("list_id", "groceries") - lists = manager.get_visible_lists(connection.user) - if list_id not in lists: - connection.send_error( - msg["id"], - "not_authorized", - f"You do not have access to list '{list_id}'" - ) - return try: products = await manager.async_get_products(list_id=list_id) @@ -274,14 +277,6 @@ async def websocket_get_active( """ manager = hass.data[DOMAIN]["manager"] list_id = msg.get("list_id", "groceries") - lists = manager.get_visible_lists(connection.user) - if list_id not in lists: - connection.send_error( - msg["id"], - "not_authorized", - f"You do not have access to list '{list_id}'" - ) - return try: active = await manager.async_get_active(list_id=list_id) @@ -320,14 +315,6 @@ async def websocket_delete_product( """ manager = hass.data[DOMAIN]["manager"] list_id = msg.get("list_id", "groceries") - lists = manager.get_visible_lists(connection.user) - if list_id not in lists: - connection.send_error( - msg["id"], - "not_authorized", - f"You do not have access to list '{list_id}'" - ) - return try: await manager.async_delete_product(list_id=list_id, key=msg["key"]) From 8b2197794a643250302074ff83feb70d0c1f1393 Mon Sep 17 00:00:00 2001 From: thekiwismarthome <134335563+thekiwismarthome@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:03:08 +1300 Subject: [PATCH 5/5] Update manager.py --- .../shopping_list_manager/manager.py | 60 +++++++++---------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/custom_components/shopping_list_manager/manager.py b/custom_components/shopping_list_manager/manager.py index 914bcf8..2dfd07f 100644 --- a/custom_components/shopping_list_manager/manager.py +++ b/custom_components/shopping_list_manager/manager.py @@ -160,34 +160,13 @@ class ShoppingListManager: async def _ensure_lists_loaded(self) -> None: data = await self._store_lists.async_load() if isinstance(data, dict): - import time - - # Migration: ensure new metadata fields exist - for list_id, meta in data.items(): - if "owner" not in meta: - meta["owner"] = "system" - if "visibility" not in meta: - meta["visibility"] = "shared" - if "created_at" not in meta: - meta["created_at"] = time.time() - if "updated_at" not in meta: - meta["updated_at"] = time.time() - self._lists = data - await self._store_lists.async_save(self._lists) return - # Default: list_id == catalogue_id (current behavior) - import time - self._lists = { "groceries": { "catalogue": "groceries", - "owner": "system", - "visibility": "shared", - "created_at": time.time(), - "updated_at": time.time(), } } @@ -228,6 +207,34 @@ class ShoppingListManager: # PUBLIC API - All operations enforce invariants # ======================================================================== + import time + + async def async_create_list( + self, + list_id: str, + catalogue: str, + owner: str, + visibility: str = "shared", + ): + await self._ensure_catalogues_loaded() + await self._ensure_lists_loaded() + + if list_id in self._lists: + raise ValueError(f"List '{list_id}' already exists") + + if catalogue not in self._catalogues: + raise ValueError(f"Catalogue '{catalogue}' does not exist") + + self._lists[list_id] = { + "catalogue": catalogue, + "owner": owner, + "visibility": visibility, + "created_at": time.time(), + "updated_at": time.time(), + } + + await self._store_lists.async_save(self._lists) + async def async_add_product( self, list_id: str, @@ -357,17 +364,6 @@ class ShoppingListManager: def get_catalogues(self) -> Dict[str, dict]: return self._catalogues - - def get_visible_lists(self, user): - if user.is_admin: - return self._lists - - return { - lid: meta - for lid, meta in self._lists.items() - if meta.get("visibility") == "shared" - or meta.get("owner") == user.id - } async def async_get_lists(self) -> Dict[str, dict]: await self._ensure_catalogues_loaded()