Compare commits

..

15 Commits

Author SHA1 Message Date
thekiwismarthome 5e043d45f2 v1.5.0 - Added dropdown list for Lists 2026-02-10 23:40:30 +13:00
thekiwismarthome 17cb680f1f Downgrade version from 1.4.0 to 1.0.0 2026-02-10 11:06:50 +13:00
thekiwismarthome 94450a4450 Update installation instructions and add Shopping List Card
Added a section for the Shopping List Card integration.
2026-02-10 11:06:20 +13:00
thekiwismarthome ae11549469 Revise installation instructions in README.md
Updated installation instructions and added manual installation steps.
2026-02-10 09:21:20 +13:00
thekiwismarthome 5429b97605 Delete custom_components/shopping_list_manager/frontend directory
splitting Integration and Card
2026-02-10 09:09:40 +13:00
thekiwismarthome 0664f5331a Update manifest.json 2026-02-06 23:56:43 +13:00
thekiwismarthome 30d8d8defd Bump version to 1.3.0 in manifest.json 2026-02-06 23:52:37 +13:00
thekiwismarthome 07329323bf Refactor configuration handling and improve product filtering
Refactor setConfig method for improved validation and normalization of configuration. Update fuzzy matching and rendering logic for inactive products.
2026-02-06 23:51:53 +13:00
thekiwismarthome 7cf703a9ac Update manifest.json 2026-02-06 16:19:06 +13:00
thekiwismarthome 1edf4b8eea Update manifest.json 2026-02-06 16:08:21 +13:00
thekiwismarthome 6376c5148d Update README.md 2026-02-06 09:48:41 +13:00
thekiwismarthome 54cbcd5298 Bump version to 1.1.0 in manifest.json 2026-02-06 09:22:47 +13:00
thekiwismarthome 5c093d2f3b Delete shopping_list_card.js 2026-02-06 09:19:53 +13:00
thekiwismarthome aef90630db Update shopping_list_card.js 2026-02-06 09:19:27 +13:00
thekiwismarthome a79e4e7171 Create shopping_list_card.js 2026-02-06 09:19:03 +13:00
7 changed files with 151 additions and 2231 deletions
+20 -23
View File
@@ -1,36 +1,33 @@
## Installation ## 1. Installation (HACS)
### 1. Install via HACS ### Recommended
[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=thekiwismarthome&repository=shopping-list-manager&category=integration) [![Open your Home Assistant instance and open this repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=thekiwismarthome&repository=shopping-list-manager&category=integration)
Click the button above or manually add the custom repository in HACS. 1. Click the button above.
Restart HA 2. Confirm adding the repository to HACS.
3. Install **Shopping List Manager** from **HACS → Integrations**.
4. Restart Home Assistant.
### 2. Add the Card Resource ---
After installing via HACS, add the frontend resource: ### Manual Repository URL
1. Go to Settings → Dashboards https://github.com/thekiwismarthome/shopping-list-manager
2. Click ⋮ (three dots, top right) → Resources
3. Click "+ Add Resource"
4. URL: `/local/community/shopping-list-manager/shopping_list_card.js`
5. Resource type: JavaScript Module
6. Click "Create"
### 3. Add the Integration Repository type: **Integration**
[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=shopping_list_manager) ---
Click the button above or manually add via Settings → Devices & Services. ## 2. Manual Installation (Optional)
### 4. Add Card to Dashboard 1. Copy the folder:
```yaml custom_components/shopping_list_manager
type: custom:shopping-list-card
title: Shopping List
list_id: groceries
```
2. Paste it into:
/config/custom_components/
Use the ⚙️ cog button in the card to configure settings. 3. Restart Home Assistant.
## 3. Shopping List Card to go with this Integration
[![Open your Home Assistant instance and open this repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=thekiwismarthome&repository=shopping-list-manager-card&category=plugin)
@@ -16,6 +16,8 @@ from .websocket_api import (
websocket_get_products, websocket_get_products,
websocket_get_active, websocket_get_active,
websocket_delete_product, websocket_delete_product,
ws_get_catalogues,
ws_get_lists,
) )
_LOGGER = logging.getLogger(__name__) _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_products)
ha_websocket.async_register_command(hass, websocket_get_active) ha_websocket.async_register_command(hass, websocket_get_active)
ha_websocket.async_register_command(hass, websocket_delete_product) 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 return True
@@ -6,6 +6,7 @@ DOMAIN = "shopping_list_manager"
STORAGE_VERSION = 1 STORAGE_VERSION = 1
STORAGE_KEY_PRODUCTS = f"{DOMAIN}.products" STORAGE_KEY_PRODUCTS = f"{DOMAIN}.products"
STORAGE_KEY_ACTIVE = f"{DOMAIN}.active_list" STORAGE_KEY_ACTIVE = f"{DOMAIN}.active_list"
LISTS_STORE_KEY = "shopping_list_manager.lists"
# Events # Events
EVENT_SHOPPING_LIST_UPDATED = f"{DOMAIN}_updated" EVENT_SHOPPING_LIST_UPDATED = f"{DOMAIN}_updated"
@@ -5,6 +5,8 @@ from typing import Dict, Optional, List
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import storage from homeassistant.helpers import storage
from homeassistant.helpers.storage import Store
from .const import ( from .const import (
DOMAIN, DOMAIN,
@@ -58,7 +60,21 @@ class ShoppingListManager:
self._store_active["groceries"] = storage.Store( self._store_active["groceries"] = storage.Store(
hass, STORAGE_VERSION, STORAGE_KEY_ACTIVE # "shopping_list_manager.active_list" 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: def _lock_for(self, list_id: str) -> asyncio.Lock:
"""Get or create lock for a list.""" """Get or create lock for a list."""
if list_id not in self._locks: if list_id not in self._locks:
@@ -68,10 +84,20 @@ class ShoppingListManager:
def _store_products_for(self, list_id: str) -> storage.Store: def _store_products_for(self, list_id: str) -> storage.Store:
"""Get or create products Store for a list.""" """Get or create products Store for a list."""
if list_id not in self._store_products: if list_id not in self._store_products:
# Non-groceries lists use namespaced keys catalogue_id = self._lists.get(list_id, {}).get("catalogue", list_id)
key = f"{DOMAIN}.{list_id}.products" catalogue = self._catalogues.get(catalogue_id)
self._store_products[list_id] = storage.Store(self.hass, STORAGE_VERSION, key)
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] return self._store_products[list_id]
def _store_active_for(self, list_id: str) -> storage.Store: def _store_active_for(self, list_id: str) -> storage.Store:
"""Get or create active Store for a list.""" """Get or create active Store for a list."""
@@ -82,6 +108,17 @@ class ShoppingListManager:
async def _ensure_loaded(self, list_id: str) -> None: async def _ensure_loaded(self, list_id: str) -> None:
"""Lazily load a list from storage if not yet in memory.""" """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: if list_id in self._products:
return # already loaded return # already loaded
@@ -103,6 +140,38 @@ class ShoppingListManager:
list_id, len(self._products[list_id]), len(self._active_list[list_id]) 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: async def async_load(self) -> None:
"""Pre-load the default groceries list for backward compat.""" """Pre-load the default groceries list for backward compat."""
async with self._lock_for("groceries"): async with self._lock_for("groceries"):
@@ -265,6 +334,15 @@ class ShoppingListManager:
_LOGGER.debug("List '%s': deleted product: %s", list_id, key) _LOGGER.debug("List '%s': deleted product: %s", list_id, key)
self._fire_update_event() 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]: async def async_get_products(self, list_id: str) -> Dict[str, dict]:
""" """
Get all products in a list's catalog. Get all products in a list's catalog.
@@ -1,7 +1,7 @@
{ {
"domain": "shopping_list_manager", "domain": "shopping_list_manager",
"name": "Shopping List Manager", "name": "Shopping List Manager",
"version": "1.0.0", "version": "1.5.0",
"documentation": "https://github.com/thekiwismarthome/shopping-list-manager", "documentation": "https://github.com/thekiwismarthome/shopping-list-manager",
"issue_tracker": "https://github.com/thekiwismarthome/shopping-list-manager/issues", "issue_tracker": "https://github.com/thekiwismarthome/shopping-list-manager/issues",
"requirements": [], "requirements": [],
@@ -74,6 +74,48 @@ async def websocket_add_product(
connection.send_error(msg["id"], "add_product_failed", str(err)) 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({ @websocket_api.websocket_command({
vol.Required("type"): "shopping_list_manager/set_qty", vol.Required("type"): "shopping_list_manager/set_qty",
vol.Optional("list_id", default="groceries"): str, vol.Optional("list_id", default="groceries"): str,
File diff suppressed because it is too large Load Diff