A supply nobody could read is unknown, not empty
This commit is contained in:
@@ -68,19 +68,34 @@ def derivecolor(name: str, tagcolor: Optional[str] = None) -> str:
|
||||
return "none"
|
||||
|
||||
|
||||
def classifysupply(level: float, name: str, vendor: Optional[str]) -> Dict:
|
||||
"""Classify one supply item into ok/low/critical.
|
||||
def classifysupply(level: Optional[float], name: str,
|
||||
vendor: Optional[str]) -> Dict:
|
||||
"""Classify one supply item into ok/low/critical, or unknown.
|
||||
|
||||
Waste cartridge fill is inverted vs a toner level: a full waste cartridge
|
||||
is bad. Standard vendors report waste as percent FULL (high = bad). Xerox
|
||||
EC/AltaLink series report waste as percent capacity REMAINING (low = bad),
|
||||
same direction as toner. Normalise everything to percent remaining first.
|
||||
|
||||
A level of None means Zabbix has no usable reading; the status is 'unknown'
|
||||
and remaining is None. Callers must not treat that as a low supply.
|
||||
"""
|
||||
lowername = (name or "").lower()
|
||||
iswaste = "waste" in lowername
|
||||
isdrum = "drum" in lowername or "imaging" in lowername
|
||||
isxerox = bool(vendor) and "xerox" in vendor.lower()
|
||||
|
||||
# No reading is its own answer, not a level. It cannot be classified ok,
|
||||
# low or critical without asserting something nobody measured - and every
|
||||
# one of those three is a claim the reader would act on.
|
||||
if level is None:
|
||||
return {
|
||||
"status": "unknown",
|
||||
"remaining": None,
|
||||
"iswaste": iswaste,
|
||||
"isdrum": isdrum,
|
||||
}
|
||||
|
||||
if iswaste and not isxerox:
|
||||
remaining = 100 - level
|
||||
else:
|
||||
|
||||
@@ -208,18 +208,64 @@ class ZabbixService:
|
||||
continue
|
||||
if str(item.get("state", "0")) != "0":
|
||||
continue
|
||||
try:
|
||||
level = int(float(item.get("lastvalue", 0)))
|
||||
except (ValueError, TypeError):
|
||||
level = 0
|
||||
level, lastseen = self._readlevel(item)
|
||||
supplies.append({
|
||||
"name": item.get("name", "Unknown"),
|
||||
"level": level,
|
||||
"lastseen": lastseen,
|
||||
"color": self._extract_color(item),
|
||||
"itemid": item.get("itemid"),
|
||||
})
|
||||
return supplies
|
||||
|
||||
# How old the newest reading may be and still be presented as the level.
|
||||
# Generous on purpose: supply items are polled in hours, not seconds, and
|
||||
# calling a slow poller "unknown" would be its own lie. This exists for the
|
||||
# host that stopped answering, not for one that answers rarely.
|
||||
STALE_AFTER_SECONDS = 24 * 3600
|
||||
|
||||
@classmethod
|
||||
def _readlevel(cls, item):
|
||||
"""(level, lastseen) for one supply item; level is None when unknown.
|
||||
|
||||
An item that has never collected returns lastvalue as an EMPTY STRING,
|
||||
and one whose host went quiet keeps a lastvalue that is arbitrarily old.
|
||||
Both used to become 0 - the empty string because float('') raises and
|
||||
the handler substituted zero, the stale one because nothing ever read
|
||||
lastclock, which was fetched and then ignored.
|
||||
|
||||
Zero is not a safe stand-in for "no reading". It is a legitimate value
|
||||
meaning the cartridge is spent, so the substitution was indistinguishable
|
||||
from a real empty - and it hit every item on the host at once, which is
|
||||
what a printer that has gone dark looks like. Downstream that reached
|
||||
`level <= EMPTY_LEVEL`, so the forecast gave it daysleft 0 and the order
|
||||
list asked purchasing to buy a full set of cartridges for a printer
|
||||
nobody had measured.
|
||||
|
||||
None says "unknown" and every consumer already handles it: the detail
|
||||
page renders N/A, the alert loop skips it, and analyse() falls back to
|
||||
the last real level in history rather than inventing one.
|
||||
"""
|
||||
raw = item.get("lastvalue")
|
||||
if raw is None or str(raw).strip() == "":
|
||||
return None, None
|
||||
try:
|
||||
level = int(float(raw))
|
||||
except (ValueError, TypeError):
|
||||
return None, None
|
||||
|
||||
try:
|
||||
lastseen = int(item.get("lastclock") or 0)
|
||||
except (ValueError, TypeError):
|
||||
lastseen = 0
|
||||
if lastseen <= 0:
|
||||
# A value with no clock cannot be aged. Trust the value and say the
|
||||
# age is unknown rather than discarding a reading that may be fine.
|
||||
return level, None
|
||||
if time.time() - lastseen > cls.STALE_AFTER_SECONDS:
|
||||
return None, lastseen
|
||||
return level, lastseen
|
||||
|
||||
# Per item, per call. Zabbix applies `limit` to the WHOLE result set, not
|
||||
# per item, so the budget is multiplied by the number of items asked for.
|
||||
HISTORY_PER_ITEM = 2000
|
||||
|
||||
Reference in New Issue
Block a user