Compare commits

...

17 Commits

Author SHA1 Message Date
thekiwismarthome 03249df651 Merge pull request #3 from thekiwismarthome/v2.0.4---Lots-of-Enhancements
V2.0.4 - Bug Fixes and New Features
2026-02-25 22:40:58 +13:00
thekiwismarthome 11180db0e3 feat: rebuild product search engine immediately after adding a new product 2026-02-25 21:54:33 +13:00
thekiwismarthome 9bdaea0b1b refactor: Replace storage.get_all_products() with storage.get_products() for product retrieval. 2026-02-25 08:19:29 +13:00
thekiwismarthome ec0f44f109 style: update color codes for Health & Beauty and Baby categories. 2026-02-19 11:32:39 +13:00
thekiwismarthome 88b3f2d435 Update handlers.py 2026-02-17 20:58:37 +13:00
thekiwismarthome d93ea86d72 duplicate websocket_search_products in handlers.py 2026-02-17 20:13:17 +13:00
thekiwismarthome 5b3dcb65b4 another fix 2026-02-17 19:14:23 +13:00
thekiwismarthome 94cdede3b9 non-admin event handler 2026-02-17 18:57:40 +13:00
thekiwismarthome e6117073be Sync Bug 2026-02-17 14:53:06 +13:00
thekiwismarthome e76fad5a92 Correct Increment Handler 2026-02-16 22:17:16 +13:00
thekiwismarthome bcfde6df9a Fix the Broken Increment Handler 2026-02-16 22:00:20 +13:00
thekiwismarthome 85e0e68af9 spelling error 2026-02-16 21:39:31 +13:00
thekiwismarthome 11eb698c20 Product Items VACA Sync Bug Fix 2026-02-16 21:27:23 +13:00
thekiwismarthome 0e3fcd56f5 Update __init__.py 2026-02-16 20:14:23 +13:00
thekiwismarthome 9d8fd3f63e Update handlers.py 2026-02-16 20:09:54 +13:00
thekiwismarthome c9c1d16f08 Update handlers.py 2026-02-16 20:05:22 +13:00
thekiwismarthome 3b0cff3476 Update handlers.py 2026-02-16 20:00:58 +13:00
4 changed files with 152 additions and 45 deletions
@@ -2,11 +2,12 @@
import logging import logging
import os import os
from pathlib import Path
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN from .const import DOMAIN, EVENT_ITEM_ADDED, EVENT_ITEM_UPDATED, EVENT_ITEM_CHECKED, EVENT_ITEM_DELETED, EVENT_LIST_UPDATED, EVENT_LIST_DELETED
from .storage import ShoppingListStorage from .storage import ShoppingListStorage
from .utils.images import ImageHandler from .utils.images import ImageHandler
@@ -23,7 +24,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True return True
# In async_setup_entry function, after storage initialization:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Shopping List Manager from a config entry.""" """Set up Shopping List Manager from a config entry."""
_LOGGER.info("Setting up Shopping List Manager") _LOGGER.info("Setting up Shopping List Manager")
@@ -58,7 +58,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Register frontend resources # Register frontend resources
await _async_register_frontend(hass) await _async_register_frontend(hass)
_LOGGER.info("Shopping List Manager setup complete") # CRITICAL: Register event listeners so non-admin users can subscribe
# Without these, non-admin users cannot receive real-time updates
def _dummy_listener(event):
"""Dummy listener to enable event subscription for all users."""
pass
hass.bus.async_listen(EVENT_ITEM_ADDED, _dummy_listener)
hass.bus.async_listen(EVENT_ITEM_UPDATED, _dummy_listener)
hass.bus.async_listen(EVENT_ITEM_CHECKED, _dummy_listener)
hass.bus.async_listen(EVENT_ITEM_DELETED, _dummy_listener)
hass.bus.async_listen(EVENT_LIST_UPDATED, _dummy_listener)
hass.bus.async_listen(EVENT_LIST_DELETED, _dummy_listener)
_LOGGER.info("Shopping List Manager setup complete with event subscriptions enabled")
return True return True
@@ -88,6 +102,10 @@ async def _async_register_websocket_handlers(
from .websocket import handlers from .websocket import handlers
# Lists handlers # Lists handlers
websocket_api.async_register_command(
hass,
handlers.websocket_subscribe,
)
websocket_api.async_register_command( websocket_api.async_register_command(
hass, hass,
handlers.websocket_get_lists, handlers.websocket_get_lists,
@@ -126,6 +144,11 @@ async def _async_register_websocket_handlers(
hass, hass,
handlers.websocket_check_item, handlers.websocket_check_item,
) )
websocket_api.async_register_command(
hass,
handlers.websocket_increment_item,
)
websocket_api.async_register_command( websocket_api.async_register_command(
hass, hass,
handlers.websocket_delete_item, handlers.websocket_delete_item,
@@ -152,6 +175,11 @@ async def _async_register_websocket_handlers(
hass, hass,
handlers.websocket_search_products, handlers.websocket_search_products,
) )
websocket_api.async_register_command(
hass,
handlers.ws_get_products_by_ids,
)
websocket_api.async_register_command( websocket_api.async_register_command(
hass, hass,
handlers.websocket_get_product_suggestions, handlers.websocket_get_product_suggestions,
@@ -184,8 +212,6 @@ async def _async_register_frontend(hass: HomeAssistant) -> None:
# The frontend card registers itself independently # The frontend card registers itself independently
_LOGGER.debug("Frontend resources skipped (separate HACS module)") _LOGGER.debug("Frontend resources skipped (separate HACS module)")
_LOGGER.debug("Frontend resources registered")
def get_storage(hass: HomeAssistant) -> ShoppingListStorage: def get_storage(hass: HomeAssistant) -> ShoppingListStorage:
"""Get the storage instance from hass.data. """Get the storage instance from hass.data.
@@ -78,7 +78,7 @@
"id": "health", "id": "health",
"name": "Health & Beauty", "name": "Health & Beauty",
"icon": "mdi:heart-pulse", "icon": "mdi:heart-pulse",
"color": "#E91E63", "color": "#009688",
"sort_order": 10, "sort_order": 10,
"system": true "system": true
}, },
@@ -94,7 +94,7 @@
"id": "baby", "id": "baby",
"name": "Baby", "name": "Baby",
"icon": "mdi:baby-face", "icon": "mdi:baby-face",
"color": "#FFEB3B", "color": "#F48FB1",
"sort_order": 12, "sort_order": 12,
"system": true "system": true
}, },
@@ -460,6 +460,9 @@ class ShoppingListStorage:
) )
self._products[new_product.id] = new_product self._products[new_product.id] = new_product
await self._save_products() await self._save_products()
# Rebuild search engine so the new product is immediately searchable
products_dict = {pid: p.to_dict() for pid, p in self._products.items()}
self._search_engine = ProductSearch(products_dict)
_LOGGER.debug("Added product: %s", new_product.name) _LOGGER.debug("Added product: %s", new_product.name)
return new_product return new_product
@@ -6,6 +6,7 @@ import voluptuous as vol
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from ..const import DOMAIN
from ..const import ( from ..const import (
WS_TYPE_LISTS_GET_ALL, WS_TYPE_LISTS_GET_ALL,
@@ -27,6 +28,7 @@ from ..const import (
WS_TYPE_PRODUCTS_ADD, WS_TYPE_PRODUCTS_ADD,
WS_TYPE_PRODUCTS_UPDATE, WS_TYPE_PRODUCTS_UPDATE,
WS_TYPE_CATEGORIES_GET_ALL, WS_TYPE_CATEGORIES_GET_ALL,
WS_TYPE_SUBSCRIBE,
EVENT_ITEM_ADDED, EVENT_ITEM_ADDED,
EVENT_ITEM_UPDATED, EVENT_ITEM_UPDATED,
EVENT_ITEM_CHECKED, EVENT_ITEM_CHECKED,
@@ -43,6 +45,119 @@ _LOGGER = logging.getLogger(__name__)
# LIST HANDLERS # LIST HANDLERS
# ============================================================================= # =============================================================================
@websocket_api.websocket_command({
vol.Required("type"): WS_TYPE_SUBSCRIBE,
})
@websocket_api.async_response
async def websocket_subscribe(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict,
) -> None:
"""Subscribe to shopping list manager events via WebSocket."""
@callback
def forward_event(event):
"""Forward HA bus event to WebSocket connection."""
connection.send_message(
websocket_api.event_message(
msg["id"],
{
"event_type": event.event_type,
"data": event.data,
}
)
)
# Subscribe to all SLM events on the HA bus (backend has permission)
unsubs = []
unsubs.append(hass.bus.async_listen(EVENT_ITEM_ADDED, forward_event))
unsubs.append(hass.bus.async_listen(EVENT_ITEM_UPDATED, forward_event))
unsubs.append(hass.bus.async_listen(EVENT_ITEM_CHECKED, forward_event))
unsubs.append(hass.bus.async_listen(EVENT_ITEM_DELETED, forward_event))
unsubs.append(hass.bus.async_listen(EVENT_LIST_UPDATED, forward_event))
unsubs.append(hass.bus.async_listen(EVENT_LIST_DELETED, forward_event))
# Clean up when connection closes
connection.subscriptions[msg["id"]] = lambda: [unsub() for unsub in unsubs]
connection.send_message(websocket_api.result_message(msg["id"]))
@websocket_api.websocket_command({
vol.Required("type"): "shopping_list_manager/items/increment",
vol.Required("item_id"): str,
vol.Required("amount"): vol.Coerce(float),
})
@websocket_api.async_response
async def websocket_increment_item(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: Dict[str, Any],
) -> None:
"""Increment item quantity atomically."""
storage = get_storage(hass)
item_id = msg["item_id"]
amount = msg["amount"]
# First get current item
for list_id, items in storage._items.items():
for item in items:
if item.id == item_id:
new_quantity = item.quantity + amount
if new_quantity < 1:
new_quantity = 1
updated_item = await storage.update_item(
item_id,
quantity=new_quantity
)
if updated_item:
hass.bus.async_fire(
EVENT_ITEM_UPDATED,
{
"list_id": updated_item.list_id,
"item_id": item_id,
"item": updated_item.to_dict()
}
)
connection.send_result(msg["id"], {
"item": updated_item.to_dict()
})
return
connection.send_error(msg["id"], "not_found", "Item not found")
@websocket_api.websocket_command({
vol.Required("type"): "shopping_list_manager/products/get_by_ids",
vol.Required("product_ids"): [str],
})
@websocket_api.async_response
async def ws_get_products_by_ids(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: Dict[str, Any],
) -> None:
"""Return products matching given product IDs."""
storage = get_storage(hass)
product_ids = set(msg["product_ids"])
# Get all products from storage
all_products = storage.get_products()
products = [
product.to_dict()
for product in all_products
if product.id in product_ids
]
connection.send_result(msg["id"], {"products": products})
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
vol.Required("type"): WS_TYPE_LISTS_GET_ALL, vol.Required("type"): WS_TYPE_LISTS_GET_ALL,
@@ -545,43 +660,6 @@ def websocket_get_list_total(
# PRODUCT HANDLERS # PRODUCT HANDLERS
# ============================================================================= # =============================================================================
@websocket_api.websocket_command(
{
vol.Required("type"): WS_TYPE_PRODUCTS_SEARCH,
vol.Required("query"): str,
vol.Optional("limit", default=10): int,
vol.Optional("exclude_allergens", default=None): vol.Any(None, [str]),
vol.Optional("include_tags", default=None): vol.Any(None, [str]),
vol.Optional("substitution_group", default=None): vol.Any(None, str),
}
)
@callback
def websocket_search_products(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: Dict[str, Any],
) -> None:
"""Handle search products command with enhanced filters."""
storage = get_storage(hass)
try:
results = storage.search_products(
query=msg["query"],
limit=msg.get("limit", 10),
exclude_allergens=msg.get("exclude_allergens"),
include_tags=msg.get("include_tags"),
substitution_group=msg.get("substitution_group"),
)
connection.send_result(
msg["id"],
{"products": [product.to_dict() for product in results]}
)
except Exception as err:
_LOGGER.error("Error searching products: %s", err)
connection.send_error(msg["id"], "search_failed", str(err))
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
vol.Required("type"): "shopping_list_manager/products/substitutes", vol.Required("type"): "shopping_list_manager/products/substitutes",