Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion custom_components/rohlikcz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

_LOGGER = logging.getLogger(__name__)

PLATFORMS: list[str] = ["sensor", "binary_sensor"]
PLATFORMS: list[str] = ["sensor", "binary_sensor","todo"]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Expand Down
9 changes: 8 additions & 1 deletion custom_components/rohlikcz/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ async def add_to_cart(self, product_id: int, quantity: int) -> Dict:
"""Add a product to the shopping cart."""
product_list = [{"product_id": product_id, "quantity": quantity}]
result = await self._rohlik_api.add_to_cart(product_list)
await self.async_update()
return result

async def search_product(self, product_name: str, limit: int = 10, favourite: bool = False) -> Optional[Dict[str, Any]]:
Expand Down Expand Up @@ -95,4 +96,10 @@ async def search_and_add(self, product_name: str, quantity: int, favourite: bool
return {"success": True, "message": "", "added_to_cart": [searched_product["search_results"][0]]}

else:
return {"success": False, "message": f'No product matched when searching for "{product_name}"{' in favourites' if favourite else ''}.', "added_to_cart": []}
return {"success": False, "message": f'No product matched when searching for "{product_name}"{' in favourites' if favourite else ''}.', "added_to_cart": []}

async def delete_from_cart(self, order_field_id: str) -> Dict:
"""Delete a product from the shopping cart using orderFieldId."""
result = await self._rohlik_api.delete_from_cart(order_field_id)
await self.async_update() # Refresh data after deletion
return result
60 changes: 51 additions & 9 deletions custom_components/rohlikcz/rohlik_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ async def get_data(self):
"delivery": "/services/frontend-service/first-delivery?reasonableDeliveryTime=true",
"next_order": "/api/v3/orders/upcoming",
"announcements": "/services/frontend-service/announcements/top",
"cart": "/services/frontend-service/v2/cart",
"bags": "/api/v1/reusable-bags/user-info",
"timeslot": "/services/frontend-service/v1/timeslot-reservation",
"last_order": "/api/v3/orders/delivered?offset=0&limit=1",
Expand Down Expand Up @@ -199,6 +198,13 @@ async def get_data(self):
_LOGGER.error(f"Error fetching {endpoint}: {err}")
result[endpoint] = None

try:
result["cart"] = await self.get_cart_content(logged_in=True, session=session)

except RequestException as err:
_LOGGER.error(f"Error fetching cart: {err}")
result["cart"] = None

return result

except RequestException as err:
Expand Down Expand Up @@ -328,10 +334,10 @@ async def get_shopping_list(self, shopping_list_id=None) -> dict:
"""
Retrieve a shopping list by its ID.

Args:
:param:
shopping_list_id (str, optional): The ID of the shopping list to retrieve. Must be provided.

Returns:
:return:
dict: The shopping list details
"""

Expand All @@ -358,19 +364,19 @@ async def get_shopping_list(self, shopping_list_id=None) -> dict:
finally:
await self._run_in_executor(session.close)

async def get_cart_content(self) -> Dict:
async def get_cart_content(self, logged_in: bool = False, session = None) -> Dict:
"""
Fetches the current cart contents

:return: Dictionary with cart content
"""

cart_url = "/services/frontend-service/v2/cart-review/check-cart"
cart_url = "/services/frontend-service/v2/cart"

session = requests.Session()

try:
if not logged_in:
session = requests.Session()
await self.login(session)
try:
cart_response = await self._run_in_executor(
session.get,
f"{BASE_URL}{cart_url}",
Expand All @@ -382,7 +388,8 @@ async def get_cart_content(self) -> Dict:
_LOGGER.error(f"Request failed: {err}")
raise ValueError("Request failed")
finally:
await self._run_in_executor(session.close)
if not logged_in:
await self._run_in_executor(session.close)

data = cart_content.get("data", {})

Expand Down Expand Up @@ -410,3 +417,38 @@ async def get_cart_content(self) -> Dict:
cart_info["products"].append(product_info)

return cart_info

async def delete_from_cart(self, order_field_id: str) -> dict:
"""
Delete an item from the shopping cart using orderFieldId.

Args:
order_field_id (str): The orderFieldId of the item to delete

Returns:
dict: Response from the deletion operation
"""
session = requests.Session()

try:
await self.login(session)

delete_url = f"/services/frontend-service/v2/cart?orderFieldId={order_field_id}"

delete_response = await self._run_in_executor(
session.delete,
f"{BASE_URL}{delete_url}"
)
delete_response.raise_for_status()

try:
return delete_response.json()
except:
# Handle case where response might not be JSON
return {"success": True, "status_code": delete_response.status_code}

except RequestException as err:
_LOGGER.error(f"Error deleting item with orderFieldId {order_field_id}: {err}")
raise APIRequestFailedError(f"Failed to delete item from cart: {err}")
finally:
await self._run_in_executor(session.close)
9 changes: 4 additions & 5 deletions custom_components/rohlikcz/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,17 +632,16 @@ class CartPriceSensor(BaseEntity, SensorEntity):
@property
def native_value(self) -> float:
"""Returns total cart price."""
return self._rohlik_account.data.get('cart', {}).get('data', {}).get('totalPrice', 0.0)
return self._rohlik_account.data.get('cart', {}).get('total_price', 0.0)

@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Returns cart details."""
cart_data = self._rohlik_account.data.get('cart', {}).get('data', {})
cart_data = self._rohlik_account.data.get('cart', {})
if cart_data:
return {
"Total savings": cart_data.get('totalSavings', 0),
"Minimal Order Price": cart_data.get('minimalOrderPrice', 0),
"Can Order": cart_data.get('submitConditionPassed', False)
"Total items": cart_data.get('total_items', 0),
"Can Order": cart_data.get('can_make_order', False)
}
return None

Expand Down
134 changes: 134 additions & 0 deletions custom_components/rohlikcz/todo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Todo platform for Rohlik.cz integration."""
from __future__ import annotations

import logging
import re

from homeassistant.components.todo import (
TodoItem,
TodoItemStatus,
TodoListEntity,
TodoListEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import DOMAIN, ICON_CART
from .hub import RohlikAccount

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Rohlik shopping cart todo platform config entry."""
rohlik_hub = hass.data[DOMAIN][config_entry.entry_id]

async_add_entities([RohlikCartTodo(rohlik_hub)])


class RohlikCartTodo(TodoListEntity):
"""A Rohlik Shopping Cart TodoListEntity."""

_attr_has_entity_name = True
_attr_supported_features = (TodoListEntityFeature.CREATE_TODO_ITEM | TodoListEntityFeature.DELETE_TODO_ITEM | TodoListEntityFeature.UPDATE_TODO_ITEM)
_attr_translation_key = "shopping_cart"
_attr_icon = ICON_CART

def __init__(
self,
rohlik_hub: RohlikAccount
) -> None:
"""Initialize RohlikCartTodo."""
super().__init__()
self._rohlik_hub = rohlik_hub
self._attr_unique_id = f"{rohlik_hub.unique_id}-cart"
self._attr_name = "Rohlik Shopping Cart"
self._attr_device_info = rohlik_hub.device_info
self._cart_content = None

# Register callback for updates
rohlik_hub.register_callback(self.async_write_ha_state)

@property
def todo_items(self) -> list[TodoItem] | None:
"""Handle updated data from the hub."""
self._cart_content = self._rohlik_hub.data["cart"]

if not self._cart_content:
return None

items = []
for product in self._cart_content.get("products", []):
# Format the summary to include relevant information
summary = f"{product['name']} ({product['quantity']}) - {product['price']} Kč"

# Use cart_item_id as the unique identifier for cart items
items.append(
TodoItem(
summary=summary,
uid=str(product['cart_item_id']),
status=TodoItemStatus.NEEDS_ACTION,
description=f"Category: {product.get('category_name', '')}\n"
f"Brand: {product.get('brand', '')}\n"
f"Product ID: {product['id']}"
)
)

return items

async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add item to shopping cart.

Supports two input formats:
- "product name" (quantity defaults to 1)
- "X product name" (where X is the desired quantity)
"""

# Check if the summary starts with a number followed by a space
quantity_match = re.match(r'^(\d+)\s+(.+)$', item.summary)

if quantity_match:
# If format is "X product name"
quantity = int(quantity_match.group(1))
product_name = quantity_match.group(2)
else:
# If format is just "product name"
quantity = 1
product_name = item.summary

# If there's still quantity info in parentheses, use that instead This handles cases like "rohlík (3)" or "2 rohlíky (5)" where (5) would take precedence
parentheses_match = re.search(r'\((\d+)\)$', product_name)
if parentheses_match:
quantity = int(parentheses_match.group(1))
product_name = product_name.split('(')[0].strip()

# Search for product and add to cart
result = await self._rohlik_hub.search_and_add(product_name, quantity)

if not result or not result.get("success", False):
_LOGGER.error("Error with adding product to")
raise ServiceValidationError(f"Product not found: {product_name}")


async def async_delete_todo_items(self, uids: list[str]) -> None:
"""Delete items from the shopping cart using the dedicated delete endpoint."""
for uid in uids:
try:
# Call the new delete_from_cart method with the cart_item_id
await self._rohlik_hub.delete_from_cart(uid)
_LOGGER.error(f"Deleted item: {uid}")
except Exception as err:
_LOGGER.error("Error deleting item %s: %s", uid, err)


async def async_update_todo_item(self, item: TodoItem) -> None:
"""Update an item to the To-do list."""
pass


5 changes: 5 additions & 0 deletions custom_components/rohlikcz/translations/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@
"delivery_info": {
"name": "Informace o doručení"
}
},
"todo": {
"shopping_cart": {
"name": "Nákupní košík"
}
}
}
}
5 changes: 5 additions & 0 deletions custom_components/rohlikcz/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@
"delivery_info": {
"name": "Delivery Information"
}
},
"todo": {
"shopping_cart": {
"name": "Shopping Cart"
}
}
}
}