mirror of
https://github.com/thekiwismarthome/shopping-list-manager.git
synced 2026-06-30 21:46:30 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03249df651 | |||
| 11180db0e3 | |||
| 9bdaea0b1b | |||
| ec0f44f109 | |||
| 88b3f2d435 | |||
| d93ea86d72 | |||
| 5b3dcb65b4 | |||
| 94cdede3b9 | |||
| e6117073be | |||
| e76fad5a92 | |||
| bcfde6df9a | |||
| 85e0e68af9 | |||
| 11eb698c20 | |||
| 0e3fcd56f5 | |||
| 9d8fd3f63e | |||
| c9c1d16f08 | |||
| 3b0cff3476 |
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user