"""2D and 3D printers incl. Klipper via Moonraker. Threshold audit 2026-07-19: the AMS desiccant trigger targets the REAL percentage humidity sensor (tk 'humidity', PERCENTAGE + HUMIDITY class, exists only on hygrometer-equipped AMS units via Features.AMS_HUMIDITY) — NOT the 1-5 'humidity_index' scale sensor (distinct tk; an endswith '_humidity' match cannot hit '_humidity_index' either). Bambu publishes no official RH threshold (their desiccant status is color-based), so >40 % RH stays an editorial trip point: fresh desiccant holds an AMS at ~10-20 % RH, 40 % means it is spent. Interval audit 2026-07-19: rail/rod lubrication follows Prusa's OFFICIAL 200-print-hour maintenance interval (octoprint/prusalink); Bambu's 500 h stays editorial (different motion system, wiki unfetchable). Data module of the suggested-setups signature catalog — see ``helpers/signatures/_model.py`` for the direction semantics and the method contract (every entry cites and is verified against the integration's source; drift-probed weekly).""" from __future__ import annotations from ._model import ConsumableSignature, IntegrationSignature SIGNATURES: dict[str, IntegrationSignature] = { "bambu_lab": IntegrationSignature( name="Bambu Lab", verified="2026-07-18 @ greghesp/ha-bambulab main", source=( "greghesp/ha-bambulab definitions.py (key/translation_key " "'total_usage_hours', UnitOfTime.HOURS, TOTAL_INCREASING lifetime " "usage → usage_delta every 500 print-hours; filament remaining is " "only a tray-sensor attribute and hms/print_error are device_class " "problem → problem-sensor adoption)." ), tasks=( ConsumableSignature( ("total_usage_hours",), "Lubricate Rails and Rods", "usage_delta", delta_units=500, ), ConsumableSignature( ("total_usage_hours",), "Replace Filter", "usage_delta", delta_units=300, models=("X1C", "X1E", "P1S", "H2"), ), ConsumableSignature( ("total_usage_hours",), "Clean Carbon Rods", "usage_delta", delta_units=100, models=("X1", "P1S", "P1P"), ), ConsumableSignature( ("total_usage_hours",), "Replace Purge Wiper", "usage_delta", delta_units=300, models=("A1",), ), ConsumableSignature( ("humidity",), "Replace Desiccant", "alert_above", delta_units=40, models=("AMS",), models_exclude=("AMS Lite",), ), ), ), "octoprint": IntegrationSignature( name="OctoPrint", verified="2026-07-18 @ home-assistant/core dev", source=( "home-assistant/core homeassistant/components/octoprint/" "binary_sensor.py (OctoPrintPrintingBinarySensor named 'Printing' " "-> entity suffix _printing; no lifetime counter exists) — the " "ENGINE accumulates print time." ), tasks=( ConsumableSignature( ("printing",), "Lubricate Rails and Rods", "runtime_hours", delta_units=200, entity_domain="binary_sensor", on_states=("on",), ), ), ), "dremel_3d_printer": IntegrationSignature( name="Dremel 3D printer", verified="2026-09-27 @ home-assistant/core 2026.9 + dremel3dpy 2.1.1", source=( "home-assistant/core homeassistant/components/dremel_3d_printer/sensor.py: " "key/translation_key 'hours_used' (UnitOfTime.HOURS, DIAGNOSTIC, " "entity_registry_enabled_default=False — the suggestion appears once the user " "enables it), value = get_printer_info()['hours_used'] = dremel3dpy " "USAGE_COUNTER ('UsageCounter', the printer's lifetime usage hours, no reset) → " "usage_delta on the FDM lubrication cadence (Prusa's 200 print-hours, like the " "other FDM printers). The 'running' binary (RUNNING, no tk) would only feed an " "engine runtime — the device counter is the more direct signal." ), tasks=( ConsumableSignature( ("hours_used",), "Lubricate Rails and Rods", "usage_delta", delta_units=200, ), ), ), "prusalink": IntegrationSignature( name="PrusaLink", verified="2026-07-18 @ home-assistant/core dev", source=( "home-assistant/core homeassistant/components/prusalink/sensor.py " "(translation_key 'printer_state', ENUM incl. 'printing') — the " "ENGINE accumulates print time on the state sensor." ), tasks=( ConsumableSignature( ("printer_state",), "Lubricate Rails and Rods", "runtime_hours", delta_units=200, on_states=("printing",), ), ), ), "moonraker": IntegrationSignature( name="Moonraker (Klipper)", verified="2026-07-19 @ marcolivierarsenault/moonraker-home-assistant main sensor.py+base.py", source=( "HACS moonraker: name 'Totals Filament Used' (has_entity_name → " "suffix totals_filament_used), METERS, TOTAL_INCREASING lifetime. " "NOTE: 'Totals Print Time' is a formatted STRING — unusable. " "Nozzle interval is an editorial default (~1000 m on brass)." ), tasks=(ConsumableSignature(("totals_filament_used",), "Replace Nozzle", "usage_delta", delta_units=1000),), ), "ha_creality_ws": IntegrationSignature( name="Creality (WebSocket)", verified="2026-09-25 @ 3dg1luk43/ha_creality_ws main; re-verified 2026-09-30 (states now from utils.derive_print_state)", source=( "3dg1luk43/ha_creality_ws custom_components/ha_creality_ws/sensor.py " "PrintStatusSensor: _attr_translation_key 'print_status', no unit, states " "(utils.derive_print_state since 2026-09-26) " "off/unknown/error/self-testing/completed/paused/stopped/printing/" "processing/idle ('printing' = st == 1 with a file loaded). No lifetime " "print-hours counter exists — the ENGINE accumulates print time. K1/K2/" "Hi/3V3 are all FDM machines (Prusa's 200 print-hour lubrication cadence)." ), tasks=( ConsumableSignature( ("print_status",), "Lubricate Rails and Rods", "runtime_hours", delta_units=200, on_states=("printing",), ), ), ), "elegoo_printer": IntegrationSignature( name="Elegoo printer", verified="2026-09-25 @ danielcherubini/elegoo-homeassistant main", source=( "danielcherubini/elegoo-homeassistant custom_components/elegoo_printer/" "definitions.py _print_status_sensor: key/translation_key 'print_status', " "ENUM of ElegooPrintStatus names (incl. 'printing'). Resin printers carry " "the same tk (PRINTER_STATUS_RESIN), so the duty is gated on the FDM-only " "sibling 'Nozzle Temperature' (key nozzle_temp, has_entity_name, no tk → " "suffix _nozzle_temperature; only in PRINTER_STATUS_FDM, sensor.py adds it " "for PrinterType.FDM). The 'Total Print Time' sensor is per job, not " "lifetime — the ENGINE accumulates print time." ), tasks=( ConsumableSignature( ("print_status",), "Lubricate Rails and Rods", "runtime_hours", delta_units=200, on_states=("printing",), require_sibling_keys=("nozzle_temperature",), ), ), ), "anycubic_cloud": IntegrationSignature( name="Anycubic Cloud", verified="2026-09-25 @ Nino6689/hass-anycubic main; nozzle wear 2026-09-27 @ 7d18d53", source=( "Nino6689/hass-anycubic custom_components/anycubic_cloud/sensor.py: " "key/translation_key 'print_time_total_hrs' (UnitOfTime.HOURS, " "TOTAL_INCREASING, printer.total_print_time_hrs — lifetime print hours, " "no reset) → usage_delta. Resin (LCD) printers share the sensor, so the " "duty is gated on the FDM-only sibling tk 'curr_nozzle_temp' " "(PrinterEntityType.FDM, helpers.check_descriptor_status_not_fdm). " "2026-09-27: tk 'nozzle_wear_percent' (PERCENTAGE, DIAGNOSTIC) = coordinator " "_totals_states min(100, abrasive grams since the nozzle reset / " "const.NOZZLE_ABRASIVE_LIFE_G 1000 × 100) — wear counting UP, back to 0 via " "button tk 'reset_nozzle_wear' (CONFIG, disabled by the integration → wiring " "enables it; async_reset_nozzle zeroes the counters). Only abrasive fill (CF/GF) " "counts, so plain PLA printing never trips it — the integration's own wear " "model. Same FDM gate as the lubrication duty." ), tasks=( ConsumableSignature( ("print_time_total_hrs",), "Lubricate Rails and Rods", "usage_delta", delta_units=200, require_sibling_keys=("curr_nozzle_temp",), ), ConsumableSignature( ("nozzle_wear_percent",), "Replace Nozzle", "alert_above", delta_units=90, require_sibling_keys=("curr_nozzle_temp",), resets=(("nozzle_wear_percent", "reset_nozzle_wear"),), ), ), ), "ipp": IntegrationSignature( name="IPP printer", verified="2026-07-16 @ home-assistant/core dev", source="home-assistant/core homeassistant/components/ipp/sensor.py (marker_, translation_key 'marker', %)", tasks=( # Every marker (each ink/toner) shares translation_key "marker" — # per_entity: one task per cartridge ("Replace Ink or Toner — Cyan # marker"), so each colour is completed on its own (#145); a mono # printer's single marker keeps the plain name. ConsumableSignature(("marker",), "Replace Ink or Toner", "percent_left", per_entity=True), ), ), "brother": IntegrationSignature( name="Brother printer", verified="2026-09-25 @ home-assistant/core dev", source=( "home-assistant/core homeassistant/components/brother/sensor.py " "(*_toner_remaining / *_ink_remaining / *_remaining_life, PERCENTAGE). " "Inkjets: black/cyan/magenta/yellow_ink_remaining (since 2026.7). " "ink_capture_box_remaining_life (dev, 2026-09; strings.json 'Ink capture " "box remaining lifetime') = the waste-ink box → the maintenance-box duty. " "laser_remaining_life, pf_kit_1_remaining_life, pf_kit_mp_remaining_life " "(paper-feed kits, tray 1 / multipurpose tray)." ), tasks=( ConsumableSignature( ( "black_ink_remaining", "cyan_ink_remaining", "magenta_ink_remaining", "yellow_ink_remaining", ), "Replace Ink or Toner", "percent_left", per_entity=True, # inkjets: one task per cartridge (#145) ), ConsumableSignature(("ink_capture_box_remaining_life",), "Replace Maintenance Box", "percent_left"), ConsumableSignature(("laser_remaining_life",), "Replace Laser Unit", "percent_left"), ConsumableSignature( ("pf_kit_1_remaining_life", "pf_kit_mp_remaining_life"), "Replace Paper Feed Kit", "percent_left", per_entity=True, # the tray-1 and MP-tray kits wear independently ), ConsumableSignature( ( "black_toner_remaining", "cyan_toner_remaining", "magenta_toner_remaining", "yellow_toner_remaining", ), "Replace Toner", "percent_left", per_entity=True, # colour lasers: one task per toner (#145) ), ConsumableSignature( ( "drum_remaining_life", "black_drum_remaining_life", "cyan_drum_remaining_life", "magenta_drum_remaining_life", "yellow_drum_remaining_life", ), "Replace Drum Unit", "percent_left", per_entity=True, # per-colour drums on colour models ), ConsumableSignature(("belt_unit_remaining_life",), "Replace Belt Unit", "percent_left"), ConsumableSignature(("fuser_remaining_life",), "Replace Fuser", "percent_left"), ), ), "syncthru": IntegrationSignature( name="Samsung SyncThru printer", verified="2026-09-27 @ home-assistant/core 2026.9 + PySyncThru 0.8.0", source=( "home-assistant/core homeassistant/components/syncthru/sensor.py " "get_toner_entity_description / get_drum_entity_description: key = " "translation_key = f'toner_{color}' / f'drum_{color}' (black/cyan/magenta/" "yellow, strings.json 'Black toner level' … 'Yellow drum level'), PERCENTAGE, " "DIAGNOSTIC, value = the SyncThru status dict's 'remaining' → percent left; " "created only for supported cartridges (pysyncthru toner_status/drum_status " "filter_supported: opt != 0). No reset buttons (the printer resets its own " "counters on a new cartridge). The 'active_alerts' count and the tray " "sensors are status, skipped; the problem binary is problem-sensor adoption." ), tasks=( ConsumableSignature( ("toner_black", "toner_cyan", "toner_magenta", "toner_yellow"), "Replace Toner", "percent_left", per_entity=True, # colour lasers: one task per toner (#145) ), ConsumableSignature( ("drum_black", "drum_cyan", "drum_magenta", "drum_yellow"), "Replace Drum Unit", "percent_left", per_entity=True, # per-colour imaging units on colour models ), ), ), "hpprinter": IntegrationSignature( name="HP printer", verified="2026-09-25 @ elad-bar/ha-hpprinter master", source=( "elad-bar/ha-hpprinter custom_components/hpprinter/parameters/" "data_points.json 'consumable_percentage_level_remaining' " "(ConsumablePercentageLevelRemaining, unit %, excluded for printheads); " "managers/ha_config_manager.py sets translation_key = property_key. Each " "cartridge is its OWN device (ha_coordinator.create_consumable_device, " "via_device = the printer), so every cartridge device carries one level " "sensor and gets its own plain duty." ), tasks=( ConsumableSignature( ("consumable_percentage_level_remaining",), "Replace Ink or Toner", "percent_left", per_entity=True, ), ), ), "epson_workforce": IntegrationSignature( name="Epson WorkForce", verified="2026-09-25 @ lymanepp/ha-epson-workforce master", source=( "lymanepp/ha-epson-workforce custom_components/epson_workforce/sensor.py " "SENSOR_TYPES ink_bk/ink_pb/ink_gy/ink_m/ink_c/ink_y/ink_lc/ink_lm named " "'Ink level ' (PERCENTAGE, _attr_has_entity_name, no tk → " "entity-id suffixes _ink_level_black … _ink_level_light_magenta); " "parser.py reads the tank bar height = ink left. The 'clean' maintenance-" "box sensor ('Cleaning level') is SKIPPED: the parser only reads a bar " "height, whether it shows capacity used or left is not established." ), tasks=( ConsumableSignature( ( "ink_level_black", "ink_level_photoblack", "ink_level_gray", "ink_level_cyan", "ink_level_magenta", "ink_level_yellow", "ink_level_light_cyan", "ink_level_light_magenta", ), "Replace Ink or Toner", "percent_left", per_entity=True, # one task per tank ), ), ), }