From ae7717a8eb10ad46b9a06cf4198195954a0fc5d0 Mon Sep 17 00:00:00 2001 From: thekiwismarthome Date: Sun, 1 Mar 2026 21:20:09 +1300 Subject: [PATCH] feat: Add WebSocket endpoint to download, process, and save product images locally as WebP. --- .../shopping_list_manager/__init__.py | 4 ++ .../websocket/handlers.py | 61 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/custom_components/shopping_list_manager/__init__.py b/custom_components/shopping_list_manager/__init__.py index 64f928a..6aa18cd 100644 --- a/custom_components/shopping_list_manager/__init__.py +++ b/custom_components/shopping_list_manager/__init__.py @@ -171,6 +171,10 @@ async def _async_register_websocket_handlers( ) # Products handlers + websocket_api.async_register_command( + hass, + handlers.websocket_download_product_image, + ) websocket_api.async_register_command( hass, handlers.websocket_search_by_barcode, diff --git a/custom_components/shopping_list_manager/websocket/handlers.py b/custom_components/shopping_list_manager/websocket/handlers.py index 0892478..8f944b4 100644 --- a/custom_components/shopping_list_manager/websocket/handlers.py +++ b/custom_components/shopping_list_manager/websocket/handlers.py @@ -1,14 +1,23 @@ """WebSocket API handlers for Shopping List Manager.""" +import io import logging +import re +from pathlib import Path from typing import Any, Dict import voluptuous as vol +from aiohttp import ClientTimeout +from PIL import Image from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from ..const import DOMAIN from ..const import ( + IMAGE_SIZE, + IMAGE_QUALITY, + IMAGES_LOCAL_DIR, WS_TYPE_LISTS_GET_ALL, WS_TYPE_LISTS_CREATE, WS_TYPE_LISTS_UPDATE, @@ -782,6 +791,58 @@ def websocket_get_list_total( # PRODUCT HANDLERS # ============================================================================= +@websocket_api.websocket_command( + { + vol.Required("type"): "shopping_list_manager/products/download_image", + vol.Required("image_url"): str, + vol.Required("product_name"): str, + } +) +@websocket_api.async_response +async def websocket_download_product_image( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: Dict[str, Any], +) -> None: + """Download a remote image and save it as WebP to the local images directory.""" + raw_url: str = msg["image_url"] + product_name: str = msg["product_name"] + + safe_stem = re.sub(r"[^a-z0-9_]", "", product_name.lower().replace(" ", "_")) or "product" + filename = f"{safe_stem}.webp" + images_dir = Path(hass.config.path(IMAGES_LOCAL_DIR)) + images_dir.mkdir(parents=True, exist_ok=True) + dest = images_dir / filename + + try: + session = async_get_clientsession(hass) + async with session.get(raw_url, timeout=ClientTimeout(total=10)) as resp: + if resp.status != 200: + connection.send_error(msg["id"], "download_failed", f"HTTP {resp.status}") + return + raw = await resp.read() + except Exception as exc: # noqa: BLE001 + connection.send_error(msg["id"], "download_failed", str(exc)) + return + + try: + img = Image.open(io.BytesIO(raw)) + if img.mode not in ("RGB", "RGBA"): + img = img.convert("RGBA") + img.thumbnail((IMAGE_SIZE, IMAGE_SIZE), Image.LANCZOS) + out = io.BytesIO() + img.save(out, format="WEBP", quality=IMAGE_QUALITY) + dest.write_bytes(out.getvalue()) + except Exception as exc: # noqa: BLE001 + connection.send_error(msg["id"], "conversion_failed", str(exc)) + return + + connection.send_result( + msg["id"], + {"local_url": f"/local/shopping_list_manager/images/{filename}"}, + ) + + @websocket_api.websocket_command( { vol.Required("type"): "shopping_list_manager/products/search_by_barcode",