feat: add custom regions support

This commit is contained in:
thekiwismarthome
2026-08-09 09:45:33 +12:00
parent 19bf03a5c6
commit 78f857f8f9
4 changed files with 135 additions and 10 deletions
@@ -242,6 +242,14 @@ async def _async_register_websocket_handlers(
hass,
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
websocket_api.async_register_command(
@@ -10,6 +10,7 @@ STORAGE_KEY_ITEMS = f"{DOMAIN}.items"
STORAGE_KEY_PRODUCTS = f"{DOMAIN}.products"
STORAGE_KEY_CATEGORIES = f"{DOMAIN}.categories"
STORAGE_KEY_LOYALTY_CARDS = f"{DOMAIN}.loyalty_cards"
STORAGE_KEY_CUSTOM_REGIONS = f"{DOMAIN}.custom_regions"
# WebSocket Commands - Lists
WS_TYPE_LISTS_GET_ALL = f"{DOMAIN}/lists/get_all"
@@ -17,6 +17,7 @@ from .const import (
STORAGE_KEY_PRODUCTS,
STORAGE_KEY_CATEGORIES,
STORAGE_KEY_LOYALTY_CARDS,
STORAGE_KEY_CUSTOM_REGIONS,
IMAGES_LOCAL_DIR,
LEGACY_IMAGES_LOCAL_DIR,
LOCAL_IMAGE_URL_PREFIX,
@@ -48,12 +49,14 @@ class ShoppingListStorage:
self._store_products = Store(hass, STORAGE_VERSION, STORAGE_KEY_PRODUCTS)
self._store_categories = Store(hass, STORAGE_VERSION, STORAGE_KEY_CATEGORIES)
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._items: Dict[str, List[Item]] = {}
self._products: Dict[str, Product] = {}
self._categories: List[Category] = []
self._loyalty_cards: Dict[str, LoyaltyCard] = {}
self._custom_regions: Dict[str, str] = {} # code -> display name
self._search_engine: Optional[ProductSearch] = None
self._images_dir = Path(hass.config.path(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))
# 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
if self._products:
products_dict = {pid: p.to_dict() for pid, p in self._products.items()}
@@ -824,3 +833,33 @@ class ShoppingListStorage:
await self._save_loyalty_cards()
_LOGGER.debug("Updated members for loyalty card: %s", card_id)
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."""
country = hass.data[DOMAIN].get("country", "NZ")
version = hass.data[DOMAIN].get("version", "unknown")
storage = get_storage(hass)
built_in = {
"NZ": "New Zealand",
"AU": "Australia",
"US": "United States",
"GB": "United Kingdom",
"CA": "Canada",
"BE": "Belgium (Dutch)",
}
custom = storage.get_custom_regions()
connection.send_result(
msg["id"],
{
"country": country,
"version": version,
"available_countries": {
"NZ": "New Zealand",
"AU": "Australia",
"US": "United States",
"GB": "United Kingdom",
"CA": "Canada",
"BE": "Belgium (Dutch)",
},
"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(
{
vol.Required("type"): "shopping_list_manager/set_country",
vol.Required("country"): vol.In(_VALID_COUNTRIES),
vol.Required("country"): str,
}
)
@websocket_api.async_response
@@ -1242,6 +1246,11 @@ async def websocket_set_country(
country = msg["country"].upper()
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)
# 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
# =============================================================================