printers: low-toner alerts with configurable thresholds + support-team routing
Poll Zabbix for toner levels on a schedule and email/webhook on a downward crossing. Warning fires at or below the warning threshold (default 5%), critical at the critical threshold (default 0%); both thresholds are settings. State lives in printersupplyalerts so an alert fires once per crossing and re-arms after a refill. Recipients mirror the printedparts pattern: plugin-scoped shopdb users + roles + free-text emails (falling back to the site alert_recipients), and a chosen support team's webhook (falling back to the site alert_webhook_url). - PrinterSupplyAlert model + migration printers0002supplyalerts - alerttier(remaining, warning, critical) + check_supplies poller - flask printers check-toner-alerts CLI (run via scheduled task/cron) - printers alert settings + Low-Toner Alerts settings page - 7 tests: tier boundaries, once-per-crossing + re-arm, toner-only scope, custom thresholds, support-team webhook routing
This commit is contained in:
@@ -9,9 +9,11 @@ from flask import Flask, Blueprint
|
||||
import click
|
||||
|
||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||
from shopdb.api import db, AssetType
|
||||
from shopdb.api import db, AssetType, Setting
|
||||
|
||||
from .models import Printer, PrinterType, ModelSupply, PrinterDriver
|
||||
from .models import (
|
||||
Printer, PrinterType, ModelSupply, PrinterDriver, PrinterSupplyAlert
|
||||
)
|
||||
from .api import printers_asset_bp
|
||||
from .services import ZabbixService
|
||||
|
||||
@@ -76,6 +78,7 @@ class PrintersPlugin(BasePlugin):
|
||||
PrinterType, # printer type classification
|
||||
PrinterDriver, # driver links (SMB/HTTP)
|
||||
ModelSupply, # model -> toner/drum/waste part numbers
|
||||
PrinterSupplyAlert, # per-printer toner alert crossing state
|
||||
]
|
||||
|
||||
def get_services(self) -> Dict[str, Type]:
|
||||
@@ -103,8 +106,41 @@ class PrintersPlugin(BasePlugin):
|
||||
with app.app_context():
|
||||
self._ensure_asset_type()
|
||||
self._ensure_printer_types()
|
||||
self._seed_settings()
|
||||
logger.info("Printers plugin installed")
|
||||
|
||||
def on_enable(self, app: Flask) -> None:
|
||||
# Idempotent re-seed so settings added in later versions reach sites
|
||||
# that installed earlier (enable runs on every upgrade cycle).
|
||||
with app.app_context():
|
||||
self._seed_settings()
|
||||
|
||||
def _seed_settings(self) -> None:
|
||||
"""Seed low-toner alert recipient settings (idempotent)."""
|
||||
defaults = [
|
||||
('printers_alert_email', '', 'string',
|
||||
'Comma-separated low-toner alert recipients; empty uses the '
|
||||
'site alert_recipients'),
|
||||
('printers_alert_userids', '', 'string',
|
||||
'Comma-separated shopdb user ids whose account emails receive '
|
||||
'low-toner alerts'),
|
||||
('printers_alert_roleids', '', 'string',
|
||||
'Comma-separated role ids; every active member of these roles '
|
||||
'receives low-toner alerts'),
|
||||
('printers_alert_supportteamid', '', 'string',
|
||||
'Support team whose webhook receives low-toner alerts; empty '
|
||||
'uses the site alert_webhook_url'),
|
||||
('printers_alert_warning_threshold', '5', 'integer',
|
||||
'Toner percent remaining at or below which a warning email fires'),
|
||||
('printers_alert_critical_threshold', '0', 'integer',
|
||||
'Toner percent remaining at or below which a critical email fires'),
|
||||
]
|
||||
for key, value, valuetype, description in defaults:
|
||||
if Setting.get(key) is None:
|
||||
Setting.set(key, value, valuetype=valuetype,
|
||||
category='printers', description=description)
|
||||
db.session.commit()
|
||||
|
||||
def _ensure_asset_type(self) -> None:
|
||||
"""Ensure printer asset type exists."""
|
||||
existing = AssetType.query.filter_by(assettype='printer').first()
|
||||
@@ -151,6 +187,19 @@ class PrintersPlugin(BasePlugin):
|
||||
"""Called when plugin is uninstalled."""
|
||||
logger.info("Printers plugin uninstalled")
|
||||
|
||||
def get_settings_cards(self) -> List[dict]:
|
||||
return [
|
||||
{
|
||||
'group': 'Printers',
|
||||
'to': '/settings/printer-alerts',
|
||||
'icon': 'bell',
|
||||
'title': 'Low-Toner Alerts',
|
||||
'description': 'Who gets warning (5%) and critical (0%) toner '
|
||||
'emails, and which support team webhook',
|
||||
'position': 48,
|
||||
},
|
||||
]
|
||||
|
||||
def get_cli_commands(self) -> List:
|
||||
"""Return CLI commands for this plugin."""
|
||||
|
||||
@@ -159,6 +208,22 @@ class PrintersPlugin(BasePlugin):
|
||||
"""Printers plugin commands."""
|
||||
pass
|
||||
|
||||
@printerscli.command('check-toner-alerts')
|
||||
def checktoneralerts():
|
||||
"""Poll Zabbix for all printers and email/webhook low-toner crossings.
|
||||
|
||||
Run on a schedule (scheduled task / cron). Fires a warning at or
|
||||
below 5 percent and a critical at 0 percent, once per crossing."""
|
||||
from flask import current_app
|
||||
from .services import check_supplies
|
||||
|
||||
with current_app.app_context():
|
||||
summary = check_supplies()
|
||||
click.echo(
|
||||
f"Toner poll: {summary['polled']}/{summary['printers']} "
|
||||
f"printers reachable, {summary['alerts']} alert(s) sent, "
|
||||
f"{summary['rearmed']} re-armed.")
|
||||
|
||||
@printerscli.command('check-supplies')
|
||||
@click.argument('ip')
|
||||
def checksupplies(ip):
|
||||
|
||||
Reference in New Issue
Block a user