diff --git a/plugins/printers/services/supply_history.py b/plugins/printers/services/supply_history.py index e3e6bda..9f3b16f 100644 --- a/plugins/printers/services/supply_history.py +++ b/plugins/printers/services/supply_history.py @@ -37,6 +37,18 @@ NEW_CARTRIDGE_LEVEL = 80 # through a coarse gauge can "prove" any rate at all. MIN_POINTS_FOR_ESTIMATE = 4 +# A rate needs TIME behind it, not just readings. Supply items are often polled +# every few minutes, so four readings can span a quarter of an hour - and a 2 +# point drop across fifteen minutes extrapolates to nearly 200 percent a day, +# which is how a cartridge sitting at 82 percent gets forecast to run out in a +# fortnight. The history window can be short for the same reason: the fetch +# keeps the newest rows up to a per-item cap, so a fast-polled item returns days +# rather than the ninety asked for. +# +# Two days is the smallest span that survives a printer's daily rhythm - a +# single heavy morning does not become the whole picture. +MIN_DAYS_FOR_ESTIMATE = 2 + # Many printers report in 10% steps, so a fortnight can pass on one plateau. # Without a minimum observed drop the slope reads as zero and the forecast # says "never", which is worse than saying nothing. @@ -201,7 +213,7 @@ def burn_rate(points): if len(points) < MIN_POINTS_FOR_ESTIMATE: return None total_days = (points[-1][0] - points[0][0]).total_seconds() / 86400 - if total_days <= 0: + if total_days < MIN_DAYS_FOR_ESTIMATE: return None # The overall drop still gates the estimate: a gauge sitting on one plateau # has not proved anything yet, whatever the intervals say. @@ -290,7 +302,8 @@ def analyse(points, rise=REPLACEMENT_RISE, currentlevel=None): if rate is None: # Say which of the three it is; "no estimate" alone invites a bug report. - if len(run) < MIN_POINTS_FOR_ESTIMATE: + basis = result.get('basisdays') or 0 + if len(run) < MIN_POINTS_FOR_ESTIMATE or basis < MIN_DAYS_FOR_ESTIMATE: result['reason'] = ('replaced recently' if replacements else 'not enough history yet') else: diff --git a/tests/test_plugins/test_supply_history.py b/tests/test_plugins/test_supply_history.py index 90d17bf..0d8e1ba 100644 --- a/tests/test_plugins/test_supply_history.py +++ b/tests/test_plugins/test_supply_history.py @@ -339,3 +339,26 @@ def test_a_real_near_empty_reading_before_a_swap_is_kept(): points = normalise(series([40, 30, 5, 100, 95, 90])) assert 5.0 in [level for _, level in points] assert len(find_replacements(points)) == 1 + + +def test_minutes_of_readings_do_not_forecast_weeks(): + """Supply items are often polled every few minutes. + + Four readings a quarter of an hour apart, with a 2 point drop between the + ends, used to extrapolate to nearly 200 percent a day - so a cartridge + sitting at 82 percent was forecast to run out in a fortnight. A rate needs + time behind it, and without it the honest answer is no estimate. + """ + minutes = 5 + detail = analyse(series([84, 83, 83, 82], hours=minutes / 60)) + assert detail['burnrateperday'] is None + assert detail['daysleft'] is None + assert detail['reason'] == 'not enough history yet' + assert band(detail['daysleft']) is None # lands in "No estimate yet" + + +def test_a_run_with_enough_days_still_estimates(): + """The guard must not silence a genuinely slow, genuinely long run.""" + detail = analyse(series([84, 83, 82, 81, 80, 79])) + assert detail['burnrateperday'] == 1.0 + assert detail['daysleft'] == 79