mirror of
https://github.com/thekiwismarthome/shopping-list-manager.git
synced 2026-08-29 11:24:42 +00:00
feat: add custom regions support
This commit is contained in:
@@ -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, str] = {} # code -> display name
|
||||||
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,12 @@ 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
|
||||||
|
custom_regions_data = await self._store_custom_regions.async_load()
|
||||||
|
if custom_regions_data:
|
||||||
|
self._custom_regions = custom_regions_data.get("regions", {})
|
||||||
|
_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()}
|
||||||
@@ -824,3 +833,33 @@ 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, str]:
|
||||||
|
"""Return all custom regions as {code: display_name}."""
|
||||||
|
return dict(self._custom_regions)
|
||||||
|
|
||||||
|
async def create_custom_region(self, code: str, name: str) -> 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
|
||||||
|
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})
|
||||||
|
|||||||
@@ -1207,29 +1207,33 @@ 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()
|
||||||
|
connection.send_result(
|
||||||
|
msg["id"],
|
||||||
|
{
|
||||||
|
"country": country,
|
||||||
|
"version": version,
|
||||||
|
"available_countries": {**built_in, **custom},
|
||||||
|
"custom_regions": custom,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
_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 +1246,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 +1267,74 @@ 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)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@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()
|
||||||
|
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)
|
||||||
|
if not created:
|
||||||
|
connection.send_error(msg["id"], "conflict", f"Region {code} already exists")
|
||||||
|
return
|
||||||
|
|
||||||
|
connection.send_result(
|
||||||
|
msg["id"],
|
||||||
|
{"success": True, "code": code, "name": name, "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