peak_start = time(16, 0)
peak_end = time(21, 0)
if peak_start <= now.time() < peak_end
and now.weekday() < 5:
discharge()
Looks fine in June. Misses season flips, holidays, DST, and every utility that isn’t 4–9.
Home batteries, fleets, and grid-scale BESS — one job, off the tariff clock. Period names and boundaries. Not ¢/kWh. Not a hardcoded 4–9 PM.
Inverter apps and EMS crons are full of 16:00–21:00. That’s one California weekday in summer. Wrong in winter, on a holiday, and the week the utility refiles the tariff.
peak_start = time(16, 0)
peak_end = time(21, 0)
if peak_start <= now.time() < peak_end
and now.weekday() < 5:
discharge()
Looks fine in June. Misses season flips, holidays, DST, and every utility that isn’t 4–9.
period = results[0]["period"]
if period == "on_peak":
raise_reserve()
discharge()
elif period in ("off_peak", "super_off_peak"):
charge()
The period is the clock the utility bills. Dispatch follows it.
How it works
One REST call for the live period. A forward timeline when you need the next windows. A webhook when the period actually flips.
Any U.S. ZIP, optional street if the ZIP is shared. residential by default; commercial or industrial for behind-the-meter BESS.
off_peak, on_peak, mid_peak, super_off_peak, or critical_peak — plus next_change_at.
Charge through off-peak. Raise reserve and discharge at peak start. Hold a backup floor if the site needs one.
One ZIP. Charge through off-peak, discharge at peak. Fits Free if you refresh at next_change_at.
Aggregated homes across territories. Each ZIP has its own clock. Webhooks, not a 15-minute poll.
Fleets & grid-scaleThe retail TOU clock your EMS shouldn’t parse from PDFs. Unlimited locations is Enterprise.
Fleets & grid-scaleHome batteries
Pass the ZIP. When period is off_peak or super_off_peak, charge — from solar leftover or from the grid. When it becomes on_peak, raise reserve and discharge to the house.
Refresh at next_change_at instead of polling. That’s a few calls a day, one location — inside Free (250 calls, 3 locations). The Home Assistant recipe is that loop as sensors.
# GET /api/v1/query/current?zip_code=94103 period = results[0]["period"] next_at = results[0]["next_change_at"] # charge cheap, discharge expensive — your rates, our clock if period == "on_peak": raise_reserve() discharge() elif period in ("off_peak", "super_off_peak"): charge() refresh_at(next_at) # do not poll every 15 minutes
Free is current period only. Need tomorrow’s windows? /query/forward starts on Builder (24 h).
Fleets and grid-scale
A national pack of home batteries, a C&I portfolio, or a grid-scale BESS fleet — same job as the house, at every ZIP you operate. PG&E is not SCE. A summer weekday is not a winter holiday.
Watch locations with a signed tou.period.changed POST. Plan charge and discharge windows with /query/forward (24 h on Builder, 7 days on Growth, 30 days on Business). Unique location = distinct ZIP this billing cycle, not polls.
We still don’t send ¢/kWh. Bill savings, capacity payments, and wholesale bids stay yours. Enterprise is unlimited locations and calls from $1,200/mo, with a dedicated engineer and a contractual uptime SLA.
Battery-shaped demand response
Called events usually land in the same hours as on-peak. Use the period so a DR event doesn’t empty the pack during off-peak — and so daily TOU dispatch doesn’t fight a called event.
Charge off-peak, discharge on-peak, every day the tariff says so. That’s the bill. Most event programs pay on top of hours you would discharge anyway.
When next_change_at is an on-peak start, you already know to be full. A called event then discharges into the same window instead of a surprise drain at 2 PM.
Utility and aggregator events stay in your stack. tou.tools is the timing layer: period names and boundaries. Pair them. Don’t conflate them.
Call GET /api/v1/query/current with the site ZIP. results[0].period is the live period. Charge on off_peak / super_off_peak, discharge on on_peak, and use next_change_at to pre-position. Paid plans add /query/forward for a concrete timeline and webhooks for the flip itself.
No. tou.tools is the timing layer: period names and boundaries. Prices live in riders, baselines, and your offtake. Put your own value model on top — bill savings, capacity, wholesale, or a called event.
A house that refreshes at next_change_at fits Free (250 calls/month, 3 locations). Forward schedule starts on Builder (24 hours). Fleets count unique locations — one per ZIP — and want webhooks on paid plans. Business is about 1,500 sites. Unlimited is Enterprise; contact sales.
Yes. Aggregated home batteries and grid-scale BESS run the same job as a single home battery, at every ZIP you operate. This page is titled for that job, not “VPP” — Variable Peak Pricing is a different tariff name in our hubs.
Refresh at next_change_at, or watch the ZIP with a tou.period.changed webhook on a paid plan. A 15-minute poll is ~2,880 calls per site per month — it exhausts Free, and at fleet scale it chews through Builder too.
If we have a seeded TOU plan for the utility that serves the ZIP, yes. See coverage. When a ZIP is ambiguous (common in Texas / ERCOT), the API returns every candidate plan instead of guessing — pick the one on the bill, or pass street / city / state.
No credit card for a house. Contact sales when the fleet needs unlimited locations and an SLA.