A watt-hour figure on a mini electric bike pump is a label. A tyre count is a log. The two meet only when the tyre, the pressures, the pauses and the reason the pump stopped are written down.

This is a bench protocol for that log, plus a short Python summary. The electrical example is the CYCLAMI A2S listing on cyclami.co.uk, read on 9 October 2026. The arithmetic below is a reading of that label. It is a completed inflation count only after you fill the log.

Read the label before you run the pump

On that date the listing showed:

  • Preset pressure 0–120 PSI, measurement accuracy ±1 PSI
  • Airflow 18 L/min
  • Battery 2 × 500 mAh / 7.4 V
  • USB-C charging, about 45 minutes from a 5 V 1.5 A input
  • Pump-only weight 136 g, body 73.6 × 45 × 28.6 mm
  • Kit: extension hose, Presta adapter, Schrader adapter, ball needle, silicone case, USB-C cable

The same page's FAQ says the display can show HOT. That means over-temperature protection has stopped the pump. Let it cool before you start again, and keep skin off the outlet.

7.4 V is the nominal voltage of two lithium cells in series. Read 2 × 500 mAh / 7.4 V as a 2S pack: 500 mAh at 7.4 V. The 2 × is already inside the voltage. Multiplying the milliamp-hours by two a second time double-counts the cells.

Nameplate energy:

def nameplate_wh(milliamp_hours: float, volts: float) -> float: """Label reading. For '2 x 500 mAh / 7.4 V', pass 500 and 7.4.""" return (milliamp_hours / 1000) * volts print(round(nameplate_wh(500, 7.4), 2)) # 3.7 

That is 3.7 Wh on the label. It is the energy the printing allows you to write down. It is the energy a coulomb counter would report only after you measure the pack.

The charge line needs the same treatment. If 5.0 V and 1.5 A were held for all 45 minutes, the USB port would accept:

def usb_input_wh(volts: float, amps: float, minutes: float) -> float: return volts * amps * (minutes / 60) print(round(usb_input_wh(5.0, 1.5, 45), 3)) # 5.625 

About 5.6 Wh in at the port. Conversion loss and the taper at the end of charge mean the pack stores less than the port accepts. A 1 000 mAh reading at 7.4 V would be 7.4 Wh, which is already larger than 5.6 Wh of input. The 500 mAh reading, 3.7 Wh, fits inside that input. Use that fit as a reason to keep the 2S reading. The page says the 45 minutes and the 1.5 A as a charging description, and it does not say the current stayed at 1.5 A for the whole interval.

18 L/min is an airflow number with no back-pressure attached. Flow drops as the pressure you are pushing against rises. Dividing 18 L/min into a tyre volume does not produce a runtime.

Recheck the product page before you treat any of these figures as current. Retail pages change.

Hold the job still

One charge can look endless or empty depending on the job. A top-up from 55 to 65 PSI on a narrow road tyre moves little air. Filling a wide tyre from empty after a tube change moves a lot, and it makes more heat. Publish those two as separate rows.

For a comparable series, freeze all of these:

  • The same wheel, tyre, tube or tubeless setup, valve, hose and adapter
  • The same start pressure and the same target, both inside the tyre and rim limits
  • The same pause rule. Either no pause, or a pause you write down in seconds. A mixed rule needs its own rows
  • Ambient temperature, written on every run
  • The charge that begins the series: the stated 5 V 1.5 A input, from the display's empty indication to its full indication. Log the minutes. Skip the series if the charger is an unknown fast charger

Use a second gauge you already trust. The ±1 PSI on the listing is the maker's tolerance for the pump's own display. The reference gauge is what you record as start and end pressure.

Stop the run when the pump reaches the target, shows HOT, or will not continue. Write which of those happened. A heat stop is data. Restarting through it to inflate the number is a different, unreported test.

An inflation count that omits tyre size, start pressure, target pressure, ambient temperature and pauses is a slogan. Leave it out of the table.

The log

One row is one attempt. stopped_reason is one of target, hot, empty, abort. case_hot is yes or no from the display, not from touching the outlet. pauses_s is the cooling time you inserted. Use 0 when the rule is no pause.

run_id,ambient_c,tyre,valve,start_psi_ref,target_psi,end_psi_ref,seconds,stopped_reason,case_hot,pauses_s,notes format-only,18,700x28c,presta,30,70,70,0,target,no,0,replace every cell from the bench 

That row shows the columns. The zeros are placeholders. Replace every cell from the wheel in front of you, then add a row per attempt until the charge is finished.

Suggested file name: pump-runtime.csv.

Summarise only comparable rows

Runs with a different tyre, start, target or pause rule are a different experiment. The script groups on those four fields and counts how each group ended. It also prints the spread of seconds for the attempts that reached the target.

import csv import sys from collections import defaultdict REQUIRED = [ "run_id", "ambient_c", "tyre", "valve", "start_psi_ref", "target_psi", "end_psi_ref", "seconds", "stopped_reason", "case_hot", "pauses_s", ] REASONS = {"target", "hot", "empty", "abort"} def load(path: str) -> list[dict[str, str]]: with open(path, newline="") as handle: rows = list(csv.DictReader(handle)) if not rows: raise SystemExit("empty log") missing = [name for name in REQUIRED if name not in rows[0]] if missing: raise SystemExit("missing columns: " + ", ".join(missing)) return rows def group_key(row: dict[str, str]) -> tuple[str, str, str, str]: return ( row["tyre"].strip(), row["start_psi_ref"].strip(), row["target_psi"].strip(), row["pauses_s"].strip(), ) def median(values: list[float]) -> float: ordered = sorted(values) return ordered[len(ordered) // 2] def main(path: str) -> None: groups: dict[tuple[str, str, str, str], list[dict[str, str]]] = defaultdict(list) for row in load(path): reason = row["stopped_reason"].strip() if reason not in REASONS: raise SystemExit(f"{row['run_id']}: unknown stopped_reason {reason}") groups[group_key(row)].append(row) for key, rows in groups.items(): tyre, start, target, pauses = key finished = [row for row in rows if row["stopped_reason"].strip() == "target"] hot = [row for row in rows if row["stopped_reason"].strip() == "hot"] empty = [row for row in rows if row["stopped_reason"].strip() == "empty"] print( f"{tyre} {start}->{target} PSI, pauses_s={pauses}: " f"{len(finished)} reached target, {len(hot)} heat stops, " f"{len(empty)} empty, {len(rows)} logged" ) if finished: seconds = [float(row["seconds"]) for row in finished] print( " seconds min/median/max: " f"{min(seconds):.0f} / {median(seconds):.0f} / {max(seconds):.0f}" ) if __name__ == "__main__": main(sys.argv[1]) 

Run it with python3 runtime.py pump-runtime.csv.

A group that dies on hot while another copy of the same job dies on empty is a duty-cycle result. Report both. Averaging them into one "tyres per charge" hides the limit that actually stopped the pump.

How to publish a number

Fill this sentence from one group. Leave the brackets in the published post until the log exists:

On [date], at [ambient] °C, one full charge completed [N] inflations of a [tyre] from [start] to [target] PSI on the reference gauge before the first hot or empty row, with pauses_s=[pauses].

Until those brackets are filled from the bench, the result you can stand behind is the 3.7 Wh label reading and an empty table. A later charge, a different tyre, or a pause you added after the first HOT, is a new sentence.

The pump still does not repair a puncture. Carry the tube or the plug, and a manual mini pump when the ride is long enough that a flat battery matters.

Valve fit, wet-weather limits and what else belongs in the saddle bag are in our UK guide to a portable electric bike pump.

This article was drafted with AI assistance and checked against the cyclami.co.uk A2S listing on 9 October 2026. It describes a method. The inflation count stays blank until a log exists.