Surface add-on prices and add an add-ons total sensor

Add-on rows already carried quantity, unit and extended price; only the name
was exposed. The attribute now renders the full line and a new monetary
sensor reports the add-ons subtotal separately from the box.

The fixture totals were copied from a seven-add-on cart while the fixture
itself had one, so they are now self-consistent and a test asserts that
add-ons plus box price equals the portal's subtotal.
This commit is contained in:
flan
2026-08-03 19:15:19 +00:00
parent a97f0aec6a
commit 527f662189
10 changed files with 174 additions and 32 deletions
+9
View File
@@ -110,6 +110,15 @@ class DeliveryOrder:
def all_items(self) -> list[OrderItem]:
return [*self.items, *self.addons]
@property
def addons_total(self) -> float:
"""Combined cost of the add-ons, excluding the produce box itself.
Add-on prices are already extended (4 smoothies bill as one $17.96
line), so this is a plain sum. It should equal `subtotal - box_price`.
"""
return round(sum(a.price for a in self.addons if a.price is not None), 2)
@dataclass
class AccountSnapshot:
+7 -3
View File
@@ -1,12 +1,16 @@
{
"domain": "freshharvest",
"name": "Fresh Harvest",
"codeowners": ["@flan"],
"codeowners": [
"@flan"
],
"config_flow": true,
"documentation": "https://git.onetick.ninja/flan/ha-freshharvest",
"integration_type": "service",
"iot_class": "cloud_polling",
"issue_tracker": "https://git.onetick.ninja/flan/ha-freshharvest/issues",
"requirements": ["beautifulsoup4>=4.12"],
"version": "0.1.0"
"requirements": [
"beautifulsoup4>=4.12"
],
"version": "0.2.0"
}
+31 -6
View File
@@ -18,21 +18,30 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import FreshHarvestConfigEntry
from .api import AccountSnapshot, DeliveryOrder
from .api import AccountSnapshot, DeliveryOrder, OrderItem
from .const import DOMAIN
from .coordinator import FreshHarvestCoordinator
def _format_item(item: OrderItem, with_price: bool = False) -> str:
"""Render one line, e.g. '4 Complete Recovery Smoothie 15.2 fl oz — $17.96'."""
line = " ".join(
part for part in (str(item.quantity or ""), item.name, item.unit) if part
)
if with_price and item.price is not None:
line = f"{line} — ${item.price:,.2f}"
return line
def _items_attrs(order: DeliveryOrder | None) -> dict[str, Any] | None:
if order is None:
return None
return {
"box": order.box_name,
"produce": [
" ".join(part for part in (str(i.quantity or ""), i.name, i.unit) if part)
for i in order.items
],
"add_ons": [a.name for a in order.addons],
"box_price": order.box_price,
"produce": [_format_item(i) for i in order.items],
"add_ons": [_format_item(a, with_price=True) for a in order.addons],
"add_ons_total": order.addons_total,
}
@@ -70,6 +79,22 @@ SENSORS: tuple[FreshHarvestSensorDescription, ...] = (
"delivery_fee": s.next_order.delivery_fee,
},
),
FreshHarvestSensorDescription(
key="next_delivery_addons_total",
translation_key="next_delivery_addons_total",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement="USD",
value_fn=lambda s: s.next_order.addons_total if s.next_order else None,
attrs_fn=lambda s: None
if s.next_order is None
else {
"box_price": s.next_order.box_price,
"add_ons": [
_format_item(a, with_price=True) for a in s.next_order.addons
],
},
),
FreshHarvestSensorDescription(
key="next_delivery_items",
translation_key="next_delivery_items",
+18 -5
View File
@@ -19,11 +19,24 @@
},
"entity": {
"sensor": {
"next_delivery": { "name": "Next delivery" },
"next_delivery_total": { "name": "Next delivery total" },
"next_delivery_items": { "name": "Next delivery items" },
"open_order_delivery": { "name": "Open order delivery" },
"shop_window": { "name": "Shopping window" }
"next_delivery": {
"name": "Next delivery"
},
"next_delivery_total": {
"name": "Next delivery total"
},
"next_delivery_addons_total": {
"name": "Next delivery add-ons"
},
"next_delivery_items": {
"name": "Next delivery items"
},
"open_order_delivery": {
"name": "Open order delivery"
},
"shop_window": {
"name": "Shopping window"
}
}
}
}
@@ -19,11 +19,24 @@
},
"entity": {
"sensor": {
"next_delivery": { "name": "Next delivery" },
"next_delivery_total": { "name": "Next delivery total" },
"next_delivery_items": { "name": "Next delivery items" },
"open_order_delivery": { "name": "Open order delivery" },
"shop_window": { "name": "Shopping window" }
"next_delivery": {
"name": "Next delivery"
},
"next_delivery_total": {
"name": "Next delivery total"
},
"next_delivery_addons_total": {
"name": "Next delivery add-ons"
},
"next_delivery_items": {
"name": "Next delivery items"
},
"open_order_delivery": {
"name": "Open order delivery"
},
"shop_window": {
"name": "Shopping window"
}
}
}
}