Functional Weave
Code in TypeScript

fleet.ev-charging-cost@1.0.0

impl/python.py

3,240 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import re
from typing import Sequence

from .fleet_ev_charging_cost_types import EvChargingCost, TariffWindow
from .math_round_div import RoundingMode  ← from math.round-div ^1.0.0 · built alongside by fune
from .math_round_div_big import round_div_big  ← from math.round-div-big ^1.0.0 · built alongside by fune
from .money_amount import money  ← from money.amount ^1.0.0 · built alongside by fune

CLOCK = re.compile(r"^([01][0-9]|2[0-3]):[0-5][0-9]$")
DAY = 1440
MAX_SAFE = 9007199254740991


def _minute_of_day(name: str, text: str) -> int:
    if not isinstance(text, str) or not CLOCK.fullmatch(text):
        raise ValueError('%s must be HH:MM, 00:00 to 23:59, received "%s"' % (name, text))
    return int(text[0:2]) * 60 + int(text[3:5])


def _whole(name: str, value: int, lo: int, hi: int, rule: str) -> None:
    if isinstance(value, bool) or not isinstance(value, int) or value < lo or value > hi:
        raise ValueError("%s must be %s, received %r" % (name, rule, value))


def ev_charging_cost(
    energy_added_wh: int,
    efficiency_basis_points: int,
    charger_power_watts: int,
    start_time: str,
    tariff: Sequence[TariffWindow],
    currency: str,
    mode: RoundingMode,
) -> EvChargingCost:
    """The cost of one charge on a time-of-use tariff.

    The energy drawn is more than the energy added (the charger and battery
    lose some), and it flows at the charger's power from the start time, so it
    falls into the tariff's windows in order. Everything is counted in exact
    units of 1/(60 x efficiency) Wh, where a minute of charging is a whole
    number, so the only rounding is the final one.
    """
    _whole("energyAddedWh", energy_added_wh, 0, 1000000, "a whole number from 0 to 1000000")
    _whole("efficiencyBasisPoints", efficiency_basis_points, 1, 10000, "a whole number from 1 to 10000")
    _whole("chargerPowerWatts", charger_power_watts, 1, 1000000, "a whole number from 1 to 1000000")
    t = _minute_of_day("startTime", start_time)
    if len(tariff) == 0:
        raise ValueError("tariff must have at least one window")
    starts = []
    for window in tariff:
        start = _minute_of_day("tariff start", window.start)
        if starts and start <= starts[-1]:
            raise ValueError("tariff windows must be in ascending order of start time")
        _whole("pricePerKwhTenths", window.price_per_kwh_tenths, 0, MAX_SAFE, "a whole number, 0 or more")
        starts.append(start)
    money(0, currency)

    per_minute = charger_power_watts * efficiency_basis_points
    remaining = energy_added_wh * 600000
    total = remaining
    cost_numerator = 0
    while remaining > 0:
        index = len(starts) - 1
        for i, s in enumerate(starts):
            if s <= t:
                index = i
        nxt = starts[index + 1] if index + 1 < len(starts) else starts[0]
        length = nxt - t if nxt > t else nxt + DAY - t
        take = min(remaining, length * per_minute)
        cost_numerator += take * tariff[index].price_per_kwh_tenths
        remaining -= take
        t = (t + length) % DAY

    unit = 60 * efficiency_basis_points
    cost = int(round_div_big(str(cost_numerator), str(unit * 10000), mode))
    return EvChargingCost(
        energy_drawn_wh=int(round_div_big(str(total), str(unit), "half-up")),
        minutes=int(round_div_big(str(total), str(per_minute), "up")),
        cost=money(cost, currency),
    )