mirror of
https://github.com/thekiwismarthome/shopping-list-manager.git
synced 2026-08-30 11:34:41 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91cbc6018c | |||
| c55e90d74f | |||
| 408e360973 | |||
| 773c72d8c3 | |||
| 98eb022573 | |||
| 674a981066 | |||
| 8ac734f8e4 | |||
| 3e4f014b11 | |||
| 88cadea42f | |||
| 5e2a7e8d35 | |||
| 78f857f8f9 |
@@ -242,6 +242,14 @@ async def _async_register_websocket_handlers(
|
|||||||
hass,
|
hass,
|
||||||
handlers.websocket_set_country,
|
handlers.websocket_set_country,
|
||||||
)
|
)
|
||||||
|
websocket_api.async_register_command(
|
||||||
|
hass,
|
||||||
|
handlers.websocket_create_custom_region,
|
||||||
|
)
|
||||||
|
websocket_api.async_register_command(
|
||||||
|
hass,
|
||||||
|
handlers.websocket_delete_custom_region,
|
||||||
|
)
|
||||||
|
|
||||||
# Backup / Restore handlers
|
# Backup / Restore handlers
|
||||||
websocket_api.async_register_command(
|
websocket_api.async_register_command(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ STORAGE_KEY_ITEMS = f"{DOMAIN}.items"
|
|||||||
STORAGE_KEY_PRODUCTS = f"{DOMAIN}.products"
|
STORAGE_KEY_PRODUCTS = f"{DOMAIN}.products"
|
||||||
STORAGE_KEY_CATEGORIES = f"{DOMAIN}.categories"
|
STORAGE_KEY_CATEGORIES = f"{DOMAIN}.categories"
|
||||||
STORAGE_KEY_LOYALTY_CARDS = f"{DOMAIN}.loyalty_cards"
|
STORAGE_KEY_LOYALTY_CARDS = f"{DOMAIN}.loyalty_cards"
|
||||||
|
STORAGE_KEY_CUSTOM_REGIONS = f"{DOMAIN}.custom_regions"
|
||||||
|
|
||||||
# WebSocket Commands - Lists
|
# WebSocket Commands - Lists
|
||||||
WS_TYPE_LISTS_GET_ALL = f"{DOMAIN}/lists/get_all"
|
WS_TYPE_LISTS_GET_ALL = f"{DOMAIN}/lists/get_all"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from .const import (
|
|||||||
STORAGE_KEY_PRODUCTS,
|
STORAGE_KEY_PRODUCTS,
|
||||||
STORAGE_KEY_CATEGORIES,
|
STORAGE_KEY_CATEGORIES,
|
||||||
STORAGE_KEY_LOYALTY_CARDS,
|
STORAGE_KEY_LOYALTY_CARDS,
|
||||||
|
STORAGE_KEY_CUSTOM_REGIONS,
|
||||||
IMAGES_LOCAL_DIR,
|
IMAGES_LOCAL_DIR,
|
||||||
LEGACY_IMAGES_LOCAL_DIR,
|
LEGACY_IMAGES_LOCAL_DIR,
|
||||||
LOCAL_IMAGE_URL_PREFIX,
|
LOCAL_IMAGE_URL_PREFIX,
|
||||||
@@ -48,12 +49,14 @@ class ShoppingListStorage:
|
|||||||
self._store_products = Store(hass, STORAGE_VERSION, STORAGE_KEY_PRODUCTS)
|
self._store_products = Store(hass, STORAGE_VERSION, STORAGE_KEY_PRODUCTS)
|
||||||
self._store_categories = Store(hass, STORAGE_VERSION, STORAGE_KEY_CATEGORIES)
|
self._store_categories = Store(hass, STORAGE_VERSION, STORAGE_KEY_CATEGORIES)
|
||||||
self._store_loyalty_cards = Store(hass, STORAGE_VERSION, STORAGE_KEY_LOYALTY_CARDS)
|
self._store_loyalty_cards = Store(hass, STORAGE_VERSION, STORAGE_KEY_LOYALTY_CARDS)
|
||||||
|
self._store_custom_regions = Store(hass, STORAGE_VERSION, STORAGE_KEY_CUSTOM_REGIONS)
|
||||||
|
|
||||||
self._lists: Dict[str, ShoppingList] = {}
|
self._lists: Dict[str, ShoppingList] = {}
|
||||||
self._items: Dict[str, List[Item]] = {}
|
self._items: Dict[str, List[Item]] = {}
|
||||||
self._products: Dict[str, Product] = {}
|
self._products: Dict[str, Product] = {}
|
||||||
self._categories: List[Category] = []
|
self._categories: List[Category] = []
|
||||||
self._loyalty_cards: Dict[str, LoyaltyCard] = {}
|
self._loyalty_cards: Dict[str, LoyaltyCard] = {}
|
||||||
|
self._custom_regions: Dict[str, Any] = {} # code -> {name, currency_symbol, language}
|
||||||
self._search_engine: Optional[ProductSearch] = None
|
self._search_engine: Optional[ProductSearch] = None
|
||||||
self._images_dir = Path(hass.config.path(IMAGES_LOCAL_DIR))
|
self._images_dir = Path(hass.config.path(IMAGES_LOCAL_DIR))
|
||||||
self._legacy_images_dir = Path(hass.config.path(LEGACY_IMAGES_LOCAL_DIR))
|
self._legacy_images_dir = Path(hass.config.path(LEGACY_IMAGES_LOCAL_DIR))
|
||||||
@@ -155,6 +158,21 @@ class ShoppingListStorage:
|
|||||||
}
|
}
|
||||||
_LOGGER.debug("Loaded %d loyalty cards", len(self._loyalty_cards))
|
_LOGGER.debug("Loaded %d loyalty cards", len(self._loyalty_cards))
|
||||||
|
|
||||||
|
# Load custom regions (migrate old string-only format)
|
||||||
|
custom_regions_data = await self._store_custom_regions.async_load()
|
||||||
|
if custom_regions_data:
|
||||||
|
raw = custom_regions_data.get("regions", {})
|
||||||
|
needs_migration = any(isinstance(v, str) for v in raw.values())
|
||||||
|
self._custom_regions = {
|
||||||
|
k: (v if isinstance(v, dict) else {"name": v, "currency_symbol": None, "language": None})
|
||||||
|
for k, v in raw.items()
|
||||||
|
}
|
||||||
|
if needs_migration:
|
||||||
|
await self._save_custom_regions()
|
||||||
|
_LOGGER.info("Migrated %d custom regions to new dict format", len(self._custom_regions))
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("Loaded %d custom regions", len(self._custom_regions))
|
||||||
|
|
||||||
# Initialize search engine after products are loaded
|
# Initialize search engine after products are loaded
|
||||||
if self._products:
|
if self._products:
|
||||||
products_dict = {pid: p.to_dict() for pid, p in self._products.items()}
|
products_dict = {pid: p.to_dict() for pid, p in self._products.items()}
|
||||||
@@ -501,11 +519,7 @@ class ShoppingListStorage:
|
|||||||
_LOGGER.warning("Search engine not initialized")
|
_LOGGER.warning("Search engine not initialized")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Convert products dict to format search engine expects
|
results = self._search_engine.search(
|
||||||
products_dict = {pid: p.to_dict() for pid, p in self._products.items()}
|
|
||||||
search_engine = ProductSearch(products_dict)
|
|
||||||
|
|
||||||
results = search_engine.search(
|
|
||||||
query=query,
|
query=query,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
exclude_allergens=exclude_allergens,
|
exclude_allergens=exclude_allergens,
|
||||||
@@ -513,7 +527,6 @@ class ShoppingListStorage:
|
|||||||
substitution_group=substitution_group,
|
substitution_group=substitution_group,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert back to Product objects
|
|
||||||
return [self._products[r["id"]] for r in results if r["id"] in self._products]
|
return [self._products[r["id"]] for r in results if r["id"] in self._products]
|
||||||
|
|
||||||
def find_product_substitutes(self, product_id: str, limit: int = 5) -> List[Product]:
|
def find_product_substitutes(self, product_id: str, limit: int = 5) -> List[Product]:
|
||||||
@@ -529,10 +542,7 @@ class ShoppingListStorage:
|
|||||||
if not self._search_engine:
|
if not self._search_engine:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
products_dict = {pid: p.to_dict() for pid, p in self._products.items()}
|
results = self._search_engine.find_substitutes(product_id, limit)
|
||||||
search_engine = ProductSearch(products_dict)
|
|
||||||
|
|
||||||
results = search_engine.find_substitutes(product_id, limit)
|
|
||||||
return [self._products[r["id"]] for r in results if r["id"] in self._products]
|
return [self._products[r["id"]] for r in results if r["id"] in self._products]
|
||||||
|
|
||||||
def get_product_suggestions(self, limit: int = 20) -> List[Product]:
|
def get_product_suggestions(self, limit: int = 20) -> List[Product]:
|
||||||
@@ -824,3 +834,39 @@ class ShoppingListStorage:
|
|||||||
await self._save_loyalty_cards()
|
await self._save_loyalty_cards()
|
||||||
_LOGGER.debug("Updated members for loyalty card: %s", card_id)
|
_LOGGER.debug("Updated members for loyalty card: %s", card_id)
|
||||||
return card
|
return card
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# Custom Regions
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
def get_custom_regions(self) -> Dict[str, Any]:
|
||||||
|
"""Return all custom regions as {code: {name, currency_symbol, language}}."""
|
||||||
|
return dict(self._custom_regions)
|
||||||
|
|
||||||
|
async def create_custom_region(
|
||||||
|
self, code: str, name: str, currency_symbol: Optional[str] = None, language: Optional[str] = None
|
||||||
|
) -> bool:
|
||||||
|
"""Create a new custom region. Returns False if the code already exists."""
|
||||||
|
if code in self._custom_regions:
|
||||||
|
return False
|
||||||
|
self._custom_regions[code] = {
|
||||||
|
"name": name,
|
||||||
|
"currency_symbol": currency_symbol,
|
||||||
|
"language": language,
|
||||||
|
}
|
||||||
|
await self._save_custom_regions()
|
||||||
|
_LOGGER.debug("Created custom region: %s (%s)", code, name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def delete_custom_region(self, code: str) -> bool:
|
||||||
|
"""Delete a custom region. Returns False if not found."""
|
||||||
|
if code not in self._custom_regions:
|
||||||
|
return False
|
||||||
|
del self._custom_regions[code]
|
||||||
|
await self._save_custom_regions()
|
||||||
|
_LOGGER.debug("Deleted custom region: %s", code)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def _save_custom_regions(self) -> None:
|
||||||
|
"""Persist custom regions to storage."""
|
||||||
|
await self._store_custom_regions.async_save({"regions": self._custom_regions})
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Shopping List Manager",
|
||||||
|
"description": "Set up the Shopping List Manager integration. Country and other settings can be configured after setup via the Configure button."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Only a single instance of Shopping List Manager is allowed."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Shopping List Manager Options",
|
||||||
|
"description": "Changing country will reload the product catalog on next restart.",
|
||||||
|
"data": {
|
||||||
|
"country": "Country",
|
||||||
|
"enable_price_tracking": "Enable price tracking",
|
||||||
|
"enable_image_search": "Enable image search",
|
||||||
|
"metric_units_only": "Metric units only"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"country": "Used to localise product catalog and pricing.",
|
||||||
|
"enable_price_tracking": "Track and display product prices.",
|
||||||
|
"enable_image_search": "Search for product images automatically.",
|
||||||
|
"metric_units_only": "Show only metric units (g, kg, ml, L)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Shopping List Manager",
|
||||||
|
"description": "Richte die Shopping List Manager-Integration ein. Land und andere Einstellungen können nach der Einrichtung über die Schaltfläche Konfigurieren angepasst werden."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Es ist nur eine Instanz von Shopping List Manager erlaubt."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Shopping List Manager Optionen",
|
||||||
|
"description": "Das Ändern des Landes lädt den Produktkatalog beim nächsten Neustart neu.",
|
||||||
|
"data": {
|
||||||
|
"country": "Land",
|
||||||
|
"enable_price_tracking": "Preisverfolgung aktivieren",
|
||||||
|
"enable_image_search": "Bildsuche aktivieren",
|
||||||
|
"metric_units_only": "Nur metrische Einheiten"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"country": "Wird verwendet, um den Produktkatalog und die Preise zu lokalisieren.",
|
||||||
|
"enable_price_tracking": "Produktpreise verfolgen und anzeigen.",
|
||||||
|
"enable_image_search": "Automatisch nach Produktbildern suchen.",
|
||||||
|
"metric_units_only": "Nur metrische Einheiten anzeigen (g, kg, ml, L)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Shopping List Manager",
|
||||||
|
"description": "Configura la integración de Shopping List Manager. El país y otros ajustes se pueden configurar después de la instalación mediante el botón Configurar."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Solo se permite una instancia de Shopping List Manager."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Opciones de Shopping List Manager",
|
||||||
|
"description": "Cambiar el país recargará el catálogo de productos en el próximo reinicio.",
|
||||||
|
"data": {
|
||||||
|
"country": "País",
|
||||||
|
"enable_price_tracking": "Activar seguimiento de precios",
|
||||||
|
"enable_image_search": "Activar búsqueda de imágenes",
|
||||||
|
"metric_units_only": "Solo unidades métricas"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"country": "Se utiliza para localizar el catálogo de productos y los precios.",
|
||||||
|
"enable_price_tracking": "Realizar un seguimiento y mostrar los precios de los productos.",
|
||||||
|
"enable_image_search": "Buscar imágenes de productos automáticamente.",
|
||||||
|
"metric_units_only": "Mostrar solo unidades métricas (g, kg, ml, L)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Shopping List Manager",
|
||||||
|
"description": "Configurez l'intégration Shopping List Manager. Le pays et les autres paramètres peuvent être configurés après l'installation via le bouton Configurer."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Une seule instance de Shopping List Manager est autorisée."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Options de Shopping List Manager",
|
||||||
|
"description": "Modifier le pays rechargera le catalogue de produits au prochain redémarrage.",
|
||||||
|
"data": {
|
||||||
|
"country": "Pays",
|
||||||
|
"enable_price_tracking": "Activer le suivi des prix",
|
||||||
|
"enable_image_search": "Activer la recherche d'images",
|
||||||
|
"metric_units_only": "Unités métriques uniquement"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"country": "Utilisé pour localiser le catalogue de produits et les prix.",
|
||||||
|
"enable_price_tracking": "Suivre et afficher les prix des produits.",
|
||||||
|
"enable_image_search": "Rechercher automatiquement des images de produits.",
|
||||||
|
"metric_units_only": "Afficher uniquement les unités métriques (g, kg, ml, L)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Shopping List Manager",
|
||||||
|
"description": "Configura l'integrazione di Shopping List Manager. Il paese e le altre impostazioni possono essere configurati dopo l'installazione tramite il pulsante Configura."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "È consentita solo un'istanza di Shopping List Manager."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Opzioni di Shopping List Manager",
|
||||||
|
"description": "La modifica del paese ricaricherà il catalogo dei prodotti al prossimo riavvio.",
|
||||||
|
"data": {
|
||||||
|
"country": "Paese",
|
||||||
|
"enable_price_tracking": "Attiva il monitoraggio dei prezzi",
|
||||||
|
"enable_image_search": "Attiva la ricerca di immagini",
|
||||||
|
"metric_units_only": "Solo unità metriche"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"country": "Utilizzato per localizzare il catalogo dei prodotti e i prezzi.",
|
||||||
|
"enable_price_tracking": "Tracciare e visualizzare i prezzi dei prodotti.",
|
||||||
|
"enable_image_search": "Cercare automaticamente le immagini dei prodotti.",
|
||||||
|
"metric_units_only": "Visualizzare solo unità metriche (g, kg, ml, L)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Shopping List Manager",
|
||||||
|
"description": "Configure a integração do Shopping List Manager. O país e outras configurações podem ser ajustados após a instalação através do botão Configurar."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "Apenas uma instância do Shopping List Manager é permitida."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Opções do Shopping List Manager",
|
||||||
|
"description": "Alterar o país recarregará o catálogo de produtos no próximo reinício.",
|
||||||
|
"data": {
|
||||||
|
"country": "País",
|
||||||
|
"enable_price_tracking": "Ativar rastreamento de preços",
|
||||||
|
"enable_image_search": "Ativar pesquisa de imagens",
|
||||||
|
"metric_units_only": "Apenas unidades métricas"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"country": "Usado para localizar o catálogo de produtos e os preços.",
|
||||||
|
"enable_price_tracking": "Rastrear e exibir os preços dos produtos.",
|
||||||
|
"enable_image_search": "Pesquisar imagens de produtos automaticamente.",
|
||||||
|
"metric_units_only": "Exibir apenas unidades métricas (g, kg, ml, L)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from aiohttp import ClientTimeout
|
from aiohttp import ClientTimeout
|
||||||
@@ -1159,7 +1160,7 @@ async def websocket_off_fetch(
|
|||||||
try:
|
try:
|
||||||
if msg.get("barcode"):
|
if msg.get("barcode"):
|
||||||
barcode = msg["barcode"]
|
barcode = msg["barcode"]
|
||||||
fields = "product_name,categories_tags,image_front_thumb_url,image_front_url,image_url,price"
|
fields = "code,product_name,categories_tags,image_front_thumb_url,image_front_url,image_url,price"
|
||||||
url = f"{base_url}/api/v2/product/{barcode}.json?fields={fields}"
|
url = f"{base_url}/api/v2/product/{barcode}.json?fields={fields}"
|
||||||
async with session.get(url, timeout=ClientTimeout(total=10), headers=headers) as resp:
|
async with session.get(url, timeout=ClientTimeout(total=10), headers=headers) as resp:
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
@@ -1173,10 +1174,11 @@ async def websocket_off_fetch(
|
|||||||
else:
|
else:
|
||||||
query = msg.get("query", "")
|
query = msg.get("query", "")
|
||||||
page_size = msg.get("page_size", 5)
|
page_size = msg.get("page_size", 5)
|
||||||
fields = "product_name,categories_tags,image_front_thumb_url,image_front_url,image_url,price"
|
fields = "code,product_name,categories_tags,image_front_thumb_url,image_front_url,image_url,price"
|
||||||
url = (
|
url = (
|
||||||
f"{base_url}/api/v2/search"
|
f"{base_url}/api/v2/search"
|
||||||
f"?search_terms={query}&fields={fields}&page_size={page_size}"
|
f"?search_terms={quote_plus(query)}&fields={fields}"
|
||||||
|
f"&page_size={page_size}&sort_by=unique_scans_n"
|
||||||
)
|
)
|
||||||
async with session.get(url, timeout=ClientTimeout(total=10), headers=headers) as resp:
|
async with session.get(url, timeout=ClientTimeout(total=10), headers=headers) as resp:
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
@@ -1207,29 +1209,38 @@ def websocket_get_integration_settings(
|
|||||||
"""Return current country and available country options."""
|
"""Return current country and available country options."""
|
||||||
country = hass.data[DOMAIN].get("country", "NZ")
|
country = hass.data[DOMAIN].get("country", "NZ")
|
||||||
version = hass.data[DOMAIN].get("version", "unknown")
|
version = hass.data[DOMAIN].get("version", "unknown")
|
||||||
connection.send_result(
|
storage = get_storage(hass)
|
||||||
msg["id"],
|
built_in = {
|
||||||
{
|
|
||||||
"country": country,
|
|
||||||
"version": version,
|
|
||||||
"available_countries": {
|
|
||||||
"NZ": "New Zealand",
|
"NZ": "New Zealand",
|
||||||
"AU": "Australia",
|
"AU": "Australia",
|
||||||
"US": "United States",
|
"US": "United States",
|
||||||
"GB": "United Kingdom",
|
"GB": "United Kingdom",
|
||||||
"CA": "Canada",
|
"CA": "Canada",
|
||||||
"BE": "Belgium (Dutch)",
|
"BE": "Belgium (Dutch)",
|
||||||
},
|
}
|
||||||
|
custom = storage.get_custom_regions()
|
||||||
|
custom_names = {code: region["name"] for code, region in custom.items()}
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
options = entries[0].options if entries else {}
|
||||||
|
connection.send_result(
|
||||||
|
msg["id"],
|
||||||
|
{
|
||||||
|
"country": country,
|
||||||
|
"version": version,
|
||||||
|
"available_countries": {**built_in, **custom_names},
|
||||||
|
"custom_regions": custom,
|
||||||
|
"metric_units_only": options.get("metric_units_only", True),
|
||||||
|
"enable_price_tracking": options.get("enable_price_tracking", True),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
_VALID_COUNTRIES = ["NZ", "AU", "US", "GB", "CA", "BE"]
|
_BUILT_IN_COUNTRIES = ["NZ", "AU", "US", "GB", "CA", "BE"]
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{
|
||||||
vol.Required("type"): "shopping_list_manager/set_country",
|
vol.Required("type"): "shopping_list_manager/set_country",
|
||||||
vol.Required("country"): vol.In(_VALID_COUNTRIES),
|
vol.Required("country"): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
@@ -1242,6 +1253,11 @@ async def websocket_set_country(
|
|||||||
country = msg["country"].upper()
|
country = msg["country"].upper()
|
||||||
storage = get_storage(hass)
|
storage = get_storage(hass)
|
||||||
|
|
||||||
|
custom_regions = storage.get_custom_regions()
|
||||||
|
if country not in _BUILT_IN_COUNTRIES and country not in custom_regions:
|
||||||
|
connection.send_error(msg["id"], "invalid_country", f"Unknown region: {country}")
|
||||||
|
return
|
||||||
|
|
||||||
count = await storage.reload_catalog(country)
|
count = await storage.reload_catalog(country)
|
||||||
|
|
||||||
# Persist to HA config entry so country survives restart
|
# Persist to HA config entry so country survives restart
|
||||||
@@ -1258,6 +1274,78 @@ async def websocket_set_country(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "shopping_list_manager/regions/create",
|
||||||
|
vol.Required("code"): vol.All(str, vol.Length(min=2, max=8), vol.Upper),
|
||||||
|
vol.Required("name"): vol.All(str, vol.Length(min=1, max=64)),
|
||||||
|
vol.Optional("currency_symbol"): vol.Any(vol.All(str, vol.Length(min=1, max=5)), None),
|
||||||
|
vol.Optional("language"): vol.Any(vol.All(str, vol.Length(min=1, max=64)), None),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def websocket_create_custom_region(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: Dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Create a custom region."""
|
||||||
|
code = msg["code"].upper()
|
||||||
|
name = msg["name"].strip()
|
||||||
|
currency_symbol = msg.get("currency_symbol")
|
||||||
|
language = msg.get("language")
|
||||||
|
storage = get_storage(hass)
|
||||||
|
|
||||||
|
if code in _BUILT_IN_COUNTRIES:
|
||||||
|
connection.send_error(msg["id"], "conflict", f"{code} is a built-in region")
|
||||||
|
return
|
||||||
|
|
||||||
|
created = await storage.create_custom_region(code, name, currency_symbol, language)
|
||||||
|
if not created:
|
||||||
|
connection.send_error(msg["id"], "conflict", f"Region {code} already exists")
|
||||||
|
return
|
||||||
|
|
||||||
|
connection.send_result(
|
||||||
|
msg["id"],
|
||||||
|
{"success": True, "code": code, "custom_regions": storage.get_custom_regions()}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "shopping_list_manager/regions/delete",
|
||||||
|
vol.Required("code"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def websocket_delete_custom_region(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: Dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Delete a custom region."""
|
||||||
|
code = msg["code"].upper()
|
||||||
|
storage = get_storage(hass)
|
||||||
|
|
||||||
|
deleted = await storage.delete_custom_region(code)
|
||||||
|
if not deleted:
|
||||||
|
connection.send_error(msg["id"], "not_found", f"Custom region {code} not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# If the active country was this region, fall back to NZ
|
||||||
|
if hass.data[DOMAIN].get("country") == code:
|
||||||
|
hass.data[DOMAIN]["country"] = "NZ"
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
if entries:
|
||||||
|
entry = entries[0]
|
||||||
|
hass.config_entries.async_update_entry(entry, options={**entry.options, "country": "NZ"})
|
||||||
|
|
||||||
|
connection.send_result(
|
||||||
|
msg["id"],
|
||||||
|
{"success": True, "code": code, "custom_regions": storage.get_custom_regions()}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# BACKUP / RESTORE HANDLERS
|
# BACKUP / RESTORE HANDLERS
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user