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:
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user