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),
)